Compare commits
3 commits
master
...
better-aut
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8e0414f2f0 | ||
![]() |
4620a124d7 | ||
![]() |
64113fa4db |
250 changed files with 6143 additions and 9307 deletions
|
@ -1,6 +0,0 @@
|
||||||
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
|
|
||||||
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
|
||||||
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
|
|
||||||
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
|
|
||||||
[target.wasm32-unknown-unknown]
|
|
||||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
|
72
.github/workflows/rust.yml
vendored
72
.github/workflows/rust.yml
vendored
|
@ -16,93 +16,66 @@ 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'
|
||||||
#uses: awalsh128/cache-apt-pkgs-action@v1.2.2
|
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
|
||||||
#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 --all-targets
|
args: --locked --all-features
|
||||||
|
- 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 --all-targets
|
args: --locked
|
||||||
|
- 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 --all-targets
|
args: --locked --no-default-features --lib
|
||||||
|
|
||||||
- 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
|
||||||
|
@ -146,7 +119,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.84"
|
version: "0.2.83"
|
||||||
|
|
||||||
- run: ./sh/wasm_bindgen_check.sh --skip-setup
|
- run: ./sh/wasm_bindgen_check.sh --skip-setup
|
||||||
|
|
||||||
|
@ -156,8 +129,6 @@ 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
|
||||||
|
@ -167,45 +138,18 @@ 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
|
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,7 +1,5 @@
|
||||||
.DS_Store
|
|
||||||
**/target
|
**/target
|
||||||
**/target_ra
|
|
||||||
**/target_wasm
|
|
||||||
/.*.json
|
/.*.json
|
||||||
/.vscode
|
/.vscode
|
||||||
/media/*
|
/media/*
|
||||||
|
.DS_Store
|
||||||
|
|
29
.vscode/settings.json
vendored
29
.vscode/settings.json
vendored
|
@ -1,32 +1,5 @@
|
||||||
{
|
{
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true
|
||||||
"editor.semanticTokenColorCustomizations": {
|
|
||||||
"rules": {
|
|
||||||
"*.unsafe:rust": "#eb5046"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files.exclude": {
|
|
||||||
"target/**": true,
|
|
||||||
"target_ra/**": true,
|
|
||||||
},
|
|
||||||
// Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run`
|
|
||||||
"rust-analyzer.checkOnSave.overrideCommand": [
|
|
||||||
"cargo",
|
|
||||||
"cranky",
|
|
||||||
"--target-dir=target_ra",
|
|
||||||
"--workspace",
|
|
||||||
"--message-format=json",
|
|
||||||
"--all-targets"
|
|
||||||
],
|
|
||||||
"rust-analyzer.cargo.buildScripts.overrideCommand": [
|
|
||||||
"cargo",
|
|
||||||
"check",
|
|
||||||
"--quiet",
|
|
||||||
"--target-dir=target_ra",
|
|
||||||
"--workspace",
|
|
||||||
"--message-format=json",
|
|
||||||
"--all-targets"
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -5,63 +5,10 @@ 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 ⭐
|
|
||||||
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
|
|
||||||
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
|
|
||||||
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)).
|
|
||||||
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
|
|
||||||
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
|
|
||||||
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
|
|
||||||
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
|
|
||||||
* Add `Response::on_hover_and_drag_cursor`.
|
|
||||||
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
|
|
||||||
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
|
|
||||||
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
|
|
||||||
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
|
|
||||||
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
|
|
||||||
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
|
|
||||||
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
|
|
||||||
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
|
|
||||||
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
|
|
||||||
|
|
||||||
### Changed 🔧
|
|
||||||
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
|
|
||||||
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
|
|
||||||
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
|
|
||||||
* `DragValue` and `Slider` now use the proportional font ([#2638](https://github.com/emilk/egui/pull/2638)).
|
|
||||||
* `ScrollArea` is less aggressive about clipping its contents ([#2665](https://github.com/emilk/egui/pull/2665)).
|
|
||||||
* Updated to be compatible with a major breaking change in AccessKit that drastically reduces memory usage when accessibility is enabled ([#2678](https://github.com/emilk/egui/pull/2678)).
|
|
||||||
* Improve `DragValue` behavior ([#2649](https://github.com/emilk/egui/pull/2649), [#2650](https://github.com/emilk/egui/pull/2650), [#2688](https://github.com/emilk/egui/pull/2688), [#2638](https://github.com/emilk/egui/pull/2638)).
|
|
||||||
|
|
||||||
### Fixed 🐛
|
|
||||||
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
|
|
||||||
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
|
|
||||||
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
|
|
||||||
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
|
|
||||||
* Menus are now moved to fit on the screen.
|
|
||||||
* Fix `Window::pivot` causing windows to move around ([#2694](https://github.com/emilk/egui/pull/2694)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.1 - 2022-12-11 - Fix key-repeat
|
|
||||||
### Changed 🔧
|
|
||||||
* `InputState`: all press functions again include key repeats (like in egui 0.19) ([#2429](https://github.com/emilk/egui/pull/2429)).
|
|
||||||
* Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)).
|
|
||||||
|
|
||||||
### Fixed 🐛
|
|
||||||
* Fix key-repeats for `TextEdit`, `Slider`s, etc ([#2429](https://github.com/emilk/egui/pull/2429)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08 - AccessKit, prettier text, overlapping widgets
|
|
||||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||||
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
|
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
|
||||||
* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)).
|
* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)).
|
||||||
|
* Keyboard press events are only present at the frame when the key was pressed, consistent with how key releases work ([#2334](https://github.com/emilk/egui/pull/2334)).
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)).
|
* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)).
|
||||||
|
@ -72,42 +19,23 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
|
||||||
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||||
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
|
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||||
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
|
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
|
||||||
* Added `spacing.menu_margin` for customizing menu spacing ([#2036](https://github.com/emilk/egui/pull/2036))
|
* Added `ui.centered`.
|
||||||
* Added possibility to enable text wrap for the selected text of `egui::ComboBox` ([#2272](https://github.com/emilk/egui/pull/2272))
|
* Added possibility to enable text wrap for the selected text of `egui::ComboBox` ([#2272](https://github.com/emilk/egui/pull/2244))
|
||||||
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds ([#2270](https://github.com/emilk/egui/pull/2270)).
|
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
|
||||||
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position ([#2303](https://github.com/emilk/egui/pull/2303)).
|
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)).
|
||||||
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
||||||
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
|
||||||
* Added `panel_fill`, `window_fill` and `window_stroke` to `Visuals` for your theming pleasure ([#2406](https://github.com/emilk/egui/pull/2406)).
|
|
||||||
* Plots:
|
|
||||||
* Allow linking plot cursors ([#1722](https://github.com/emilk/egui/pull/1722)).
|
|
||||||
* Added `Plot::auto_bounds_x/y` and `Plot::reset` ([#2029](https://github.com/emilk/egui/pull/2029)).
|
|
||||||
* Added `PlotUi::translate_bounds` ([#2145](https://github.com/emilk/egui/pull/2145)).
|
|
||||||
* Added `PlotUi::set_plot_bounds` ([#2320](https://github.com/emilk/egui/pull/2320)).
|
|
||||||
* Added `PlotUi::plot_secondary_clicked` ([#2318](https://github.com/emilk/egui/pull/2318)).
|
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
||||||
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
|
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
|
||||||
* Make it slightly easier to click buttons ([#2304](https://github.com/emilk/egui/pull/2304)).
|
|
||||||
* `egui::color` has been renamed `egui::ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)).
|
|
||||||
|
|
||||||
### Fixed 🐛
|
### Fixed 🐛
|
||||||
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
|
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
|
||||||
* Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)).
|
|
||||||
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
|
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
|
||||||
* Constrain menu popups to the screen ([#2191](https://github.com/emilk/egui/pull/2191)).
|
|
||||||
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
|
||||||
* Fixed popups and color edit going outside the screen.
|
* Fixed popups and color edit going outside the screen.
|
||||||
* Fixed keyboard support in `DragValue` ([#2342](https://github.com/emilk/egui/pull/2342)).
|
* Fixed keyboard support in `DragValue` ([#2342](https://github.com/emilk/egui/pull/2342)).
|
||||||
* If you nest `ScrollAreas` inside each other, the inner area will now move its scroll bar so it is always visible ([#2371](https://github.com/emilk/egui/pull/2371)).
|
* If you nest `ScrollAreas` inside each other, the inner area will now move its scroll bar so it is always visible ([#2371](https://github.com/emilk/egui/pull/2371)).
|
||||||
* Ignore key-repeats for `input.key_pressed` ([#2334](https://github.com/emilk/egui/pull/2334), [#2389](https://github.com/emilk/egui/pull/2389)).
|
|
||||||
* Fixed issue with calling `set_pixels_per_point` each frame ([#2352](https://github.com/emilk/egui/pull/2352)).
|
|
||||||
* Fix bug in `ScrollArea::show_rows` ([#2258](https://github.com/emilk/egui/pull/2258)).
|
|
||||||
* Fix bug in `plot::Line::fill` ([#2275](https://github.com/emilk/egui/pull/2275)).
|
|
||||||
* Only emit `changed` events in `radio_value` and `selectable_value` if the value actually changed ([#2343](https://github.com/emilk/egui/pull/2343)).
|
|
||||||
* Fixed sizing bug in `Grid` ([#2384](https://github.com/emilk/egui/pull/2384)).
|
|
||||||
* `ComboBox::width` now correctly sets the outer width ([#2406](https://github.com/emilk/egui/pull/2406)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
|
@ -121,7 +49,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
|
||||||
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
|
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
|
||||||
* Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)).
|
* Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)).
|
||||||
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)).
|
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)).
|
||||||
* Added `Context::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
|
* Added `Contex::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
|
||||||
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
|
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
|
||||||
* Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)).
|
* Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)).
|
||||||
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
|
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
|
||||||
|
|
2366
Cargo.lock
generated
2366
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,10 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/ecolor",
|
|
||||||
"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",
|
||||||
|
@ -26,8 +26,7 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
|
||||||
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897
|
split-debuginfo = "unpacked" # faster debug builds on mac
|
||||||
# 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):
|
||||||
|
|
|
@ -9,10 +9,8 @@ warn = [
|
||||||
"clippy::bool_to_int_with_if",
|
"clippy::bool_to_int_with_if",
|
||||||
"clippy::char_lit_as_u8",
|
"clippy::char_lit_as_u8",
|
||||||
"clippy::checked_conversions",
|
"clippy::checked_conversions",
|
||||||
"clippy::cloned_instead_of_copied",
|
|
||||||
"clippy::dbg_macro",
|
"clippy::dbg_macro",
|
||||||
"clippy::debug_assert_with_mut_call",
|
"clippy::debug_assert_with_mut_call",
|
||||||
"clippy::derive_partial_eq_without_eq",
|
|
||||||
"clippy::disallowed_methods",
|
"clippy::disallowed_methods",
|
||||||
"clippy::disallowed_script_idents",
|
"clippy::disallowed_script_idents",
|
||||||
"clippy::doc_link_with_quotes",
|
"clippy::doc_link_with_quotes",
|
||||||
|
@ -113,9 +111,9 @@ warn = [
|
||||||
]
|
]
|
||||||
|
|
||||||
allow = [
|
allow = [
|
||||||
"clippy::manual_range_contains", # This one is just annoying
|
# TODO(emilk): enable more lints
|
||||||
|
"clippy::cloned_instead_of_copied",
|
||||||
# Some of these we should try to put in "warn":
|
"clippy::derive_partial_eq_without_eq",
|
||||||
"clippy::type_complexity",
|
"clippy::type_complexity",
|
||||||
"clippy::undocumented_unsafe_blocks",
|
"clippy::undocumented_unsafe_blocks",
|
||||||
"trivial_casts",
|
"trivial_casts",
|
||||||
|
|
32
README.md
32
README.md
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
|
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
|
||||||
|
|
||||||
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
|
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
|
||||||
|
|
||||||
egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust.
|
egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust.
|
||||||
|
|
||||||
|
@ -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 libxkbcommon-dev libssl-dev`
|
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
|
||||||
|
|
||||||
On Fedora Rawhide you need to run:
|
On Fedora Rawhide you need to run:
|
||||||
|
|
||||||
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
||||||
|
|
||||||
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
|
**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_glow/src/painter.rs))
|
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glium/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-winit` and `egui_glow` or `egui-wgpu`.
|
* [`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`.
|
||||||
|
* [`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,14 +186,12 @@ 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).
|
||||||
* [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework.
|
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) for the [ggez](https://ggez.rs/) game framework.
|
||||||
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
|
* [`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).
|
|
||||||
* [`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!
|
||||||
|
|
||||||
|
@ -324,9 +322,9 @@ If you call `.await` in your GUI code, the UI will freeze, which is very bad UX.
|
||||||
* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)
|
* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)
|
||||||
|
|
||||||
### What about accessibility, such as screen readers?
|
### What about accessibility, such as screen readers?
|
||||||
egui includes optional support for [AccessKit](https://accesskit.dev/), which currently implements the native accessibility APIs on Windows and macOS. This feature is enabled by default in eframe. For platforms that AccessKit doesn't yet support, including web, there is an experimental built-in screen reader; in [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
|
There is experimental support for a screen reader. In [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
|
||||||
|
|
||||||
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
|
Read more at <https://github.com/emilk/egui/issues/167>.
|
||||||
|
|
||||||
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
|
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
|
||||||
|
|
||||||
|
@ -372,8 +370,6 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
|
||||||
|
|
||||||
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>.
|
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.
|
||||||
|
@ -399,7 +395,6 @@ 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).
|
||||||
|
@ -412,12 +407,3 @@ Default fonts:
|
||||||
* `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md)
|
* `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md)
|
||||||
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
|
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
|
||||||
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
|
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="media/rerun_io_logo.png" width="50%">
|
|
||||||
|
|
||||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing<br>
|
|
||||||
visualizations for computer vision and robotics.
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
doc-valid-idents = ["AccessKit", ".."]
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Changelog for ecolor
|
|
||||||
All notable changes to the `ecolor` crate will be noted in this file.
|
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
|
|
||||||
## 0.21.0 - 2023-02-08
|
|
||||||
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08
|
|
||||||
* Split out `ecolor` crate from `epaint`
|
|
|
@ -1,50 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "ecolor"
|
|
||||||
version = "0.21.0"
|
|
||||||
authors = [
|
|
||||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
|
||||||
"Andreas Reich <reichandreas@gmx.de>",
|
|
||||||
]
|
|
||||||
description = "Color structs and color conversion utilities"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.65"
|
|
||||||
homepage = "https://github.com/emilk/egui"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://github.com/emilk/egui"
|
|
||||||
categories = ["mathematics", "encoding"]
|
|
||||||
keywords = ["gui", "color", "conversion", "gamedev", "images"]
|
|
||||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
|
|
||||||
## Enable additional checks if debug assertions are enabled (debug builds).
|
|
||||||
extra_debug_asserts = []
|
|
||||||
## Always enable additional checks.
|
|
||||||
extra_asserts = []
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
#! ### Optional dependencies
|
|
||||||
|
|
||||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `ecolor` types to `&[u8]`.
|
|
||||||
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
|
|
||||||
|
|
||||||
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
|
|
||||||
cint = { version = "0.3.1", optional = true }
|
|
||||||
|
|
||||||
## Enable the [`hex_color`] macro.
|
|
||||||
color-hex = { version = "0.2.0", optional = true }
|
|
||||||
|
|
||||||
## Enable this when generating docs.
|
|
||||||
document-features = { version = "0.2", optional = true }
|
|
||||||
|
|
||||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
|
|
@ -1,11 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
Made for [`egui`](https://github.com/emilk/egui/).
|
|
|
@ -1,161 +0,0 @@
|
||||||
use super::*;
|
|
||||||
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
|
|
||||||
|
|
||||||
// ---- Color32 ----
|
|
||||||
|
|
||||||
impl From<Alpha<EncodedSrgb<u8>>> for Color32 {
|
|
||||||
fn from(srgba: Alpha<EncodedSrgb<u8>>) -> Self {
|
|
||||||
let Alpha {
|
|
||||||
color: EncodedSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
Color32::from_rgba_unmultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No From<Color32> for Alpha<_> because Color32 is premultiplied
|
|
||||||
|
|
||||||
impl From<PremultipliedAlpha<EncodedSrgb<u8>>> for Color32 {
|
|
||||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<u8>>) -> Self {
|
|
||||||
let PremultipliedAlpha {
|
|
||||||
color: EncodedSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<u8>> {
|
|
||||||
fn from(col: Color32) -> Self {
|
|
||||||
let (r, g, b, a) = col.to_tuple();
|
|
||||||
|
|
||||||
PremultipliedAlpha {
|
|
||||||
color: EncodedSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PremultipliedAlpha<EncodedSrgb<f32>>> for Color32 {
|
|
||||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<f32>>) -> Self {
|
|
||||||
let PremultipliedAlpha {
|
|
||||||
color: EncodedSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
// This is a bit of an abuse of the function name but it does what we want.
|
|
||||||
let r = linear_u8_from_linear_f32(r);
|
|
||||||
let g = linear_u8_from_linear_f32(g);
|
|
||||||
let b = linear_u8_from_linear_f32(b);
|
|
||||||
let a = linear_u8_from_linear_f32(a);
|
|
||||||
|
|
||||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<f32>> {
|
|
||||||
fn from(col: Color32) -> Self {
|
|
||||||
let (r, g, b, a) = col.to_tuple();
|
|
||||||
|
|
||||||
// This is a bit of an abuse of the function name but it does what we want.
|
|
||||||
let r = linear_f32_from_linear_u8(r);
|
|
||||||
let g = linear_f32_from_linear_u8(g);
|
|
||||||
let b = linear_f32_from_linear_u8(b);
|
|
||||||
let a = linear_f32_from_linear_u8(a);
|
|
||||||
|
|
||||||
PremultipliedAlpha {
|
|
||||||
color: EncodedSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorInterop for Color32 {
|
|
||||||
type CintTy = PremultipliedAlpha<EncodedSrgb<u8>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Rgba ----
|
|
||||||
|
|
||||||
impl From<PremultipliedAlpha<LinearSrgb<f32>>> for Rgba {
|
|
||||||
fn from(srgba: PremultipliedAlpha<LinearSrgb<f32>>) -> Self {
|
|
||||||
let PremultipliedAlpha {
|
|
||||||
color: LinearSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
Rgba([r, g, b, a])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rgba> for PremultipliedAlpha<LinearSrgb<f32>> {
|
|
||||||
fn from(col: Rgba) -> Self {
|
|
||||||
let (r, g, b, a) = col.to_tuple();
|
|
||||||
|
|
||||||
PremultipliedAlpha {
|
|
||||||
color: LinearSrgb { r, g, b },
|
|
||||||
alpha: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorInterop for Rgba {
|
|
||||||
type CintTy = PremultipliedAlpha<LinearSrgb<f32>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Hsva ----
|
|
||||||
|
|
||||||
impl From<Alpha<Hsv<f32>>> for Hsva {
|
|
||||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
|
||||||
let Alpha {
|
|
||||||
color: Hsv { h, s, v },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
Hsva::new(h, s, v, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Hsva> for Alpha<Hsv<f32>> {
|
|
||||||
fn from(col: Hsva) -> Self {
|
|
||||||
let Hsva { h, s, v, a } = col;
|
|
||||||
|
|
||||||
Alpha {
|
|
||||||
color: Hsv { h, s, v },
|
|
||||||
alpha: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorInterop for Hsva {
|
|
||||||
type CintTy = Alpha<Hsv<f32>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- HsvaGamma ----
|
|
||||||
|
|
||||||
impl ColorInterop for HsvaGamma {
|
|
||||||
type CintTy = Alpha<Hsv<f32>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Alpha<Hsv<f32>>> for HsvaGamma {
|
|
||||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
|
||||||
let Alpha {
|
|
||||||
color: Hsv { h, s, v },
|
|
||||||
alpha: a,
|
|
||||||
} = srgba;
|
|
||||||
|
|
||||||
Hsva::new(h, s, v, a).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HsvaGamma> for Alpha<Hsv<f32>> {
|
|
||||||
fn from(col: HsvaGamma) -> Self {
|
|
||||||
let Hsva { h, s, v, a } = col.into();
|
|
||||||
|
|
||||||
Alpha {
|
|
||||||
color: Hsv { h, s, v },
|
|
||||||
alpha: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,216 +0,0 @@
|
||||||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba};
|
|
||||||
|
|
||||||
/// This format is used for space-efficient color representation (32 bits).
|
|
||||||
///
|
|
||||||
/// Instead of manipulating this directly it is often better
|
|
||||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
|
||||||
///
|
|
||||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
|
||||||
/// Alpha channel is in linear space.
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
|
||||||
pub struct Color32(pub(crate) [u8; 4]);
|
|
||||||
|
|
||||||
impl std::ops::Index<usize> for Color32 {
|
|
||||||
type Output = u8;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn index(&self, index: usize) -> &u8 {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::IndexMut<usize> for Color32 {
|
|
||||||
#[inline(always)]
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Color32 {
|
|
||||||
// Mostly follows CSS names:
|
|
||||||
|
|
||||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
|
||||||
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
|
||||||
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
|
|
||||||
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
|
||||||
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
|
||||||
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
|
||||||
|
|
||||||
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
|
|
||||||
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
|
||||||
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
|
||||||
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
|
|
||||||
|
|
||||||
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
|
||||||
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
|
|
||||||
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
|
|
||||||
|
|
||||||
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
|
||||||
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
|
||||||
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
|
|
||||||
|
|
||||||
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
|
|
||||||
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
|
||||||
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
|
|
||||||
|
|
||||||
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
|
||||||
|
|
||||||
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
|
|
||||||
|
|
||||||
/// An ugly color that is planned to be replaced before making it to the screen.
|
|
||||||
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
|
||||||
Self([r, g, b, 255])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
|
||||||
Self([r, g, b, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From `sRGBA` with premultiplied alpha.
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
Self([r, g, b, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
|
||||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
if a == 255 {
|
|
||||||
Self::from_rgb(r, g, b) // common-case optimization
|
|
||||||
} else if a == 0 {
|
|
||||||
Self::TRANSPARENT // common-case optimization
|
|
||||||
} else {
|
|
||||||
let r_lin = linear_f32_from_gamma_u8(r);
|
|
||||||
let g_lin = linear_f32_from_gamma_u8(g);
|
|
||||||
let b_lin = linear_f32_from_gamma_u8(b);
|
|
||||||
let a_lin = linear_f32_from_linear_u8(a);
|
|
||||||
|
|
||||||
let r = gamma_u8_from_linear_f32(r_lin * a_lin);
|
|
||||||
let g = gamma_u8_from_linear_f32(g_lin * a_lin);
|
|
||||||
let b = gamma_u8_from_linear_f32(b_lin * a_lin);
|
|
||||||
|
|
||||||
Self::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_gray(l: u8) -> Self {
|
|
||||||
Self([l, l, l, 255])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_black_alpha(a: u8) -> Self {
|
|
||||||
Self([0, 0, 0, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_white_alpha(a: u8) -> Self {
|
|
||||||
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
|
||||||
Self([l, l, l, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn is_opaque(&self) -> bool {
|
|
||||||
self.a() == 255
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn r(&self) -> u8 {
|
|
||||||
self.0[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn g(&self) -> u8 {
|
|
||||||
self.0[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn b(&self) -> u8 {
|
|
||||||
self.0[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn a(&self) -> u8 {
|
|
||||||
self.0[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an opaque version of self
|
|
||||||
pub fn to_opaque(self) -> Self {
|
|
||||||
Rgba::from(self).to_opaque().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an additive version of self
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn additive(self) -> Self {
|
|
||||||
let [r, g, b, _] = self.to_array();
|
|
||||||
Self([r, g, b, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Premultiplied RGBA
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn to_array(&self) -> [u8; 4] {
|
|
||||||
[self.r(), self.g(), self.b(), self.a()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Premultiplied RGBA
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn to_tuple(&self) -> (u8, u8, u8, u8) {
|
|
||||||
(self.r(), self.g(), self.b(), self.a())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
|
||||||
Rgba::from(*self).to_srgba_unmultiplied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply with 0.5 to make color half as opaque, perceptually.
|
|
||||||
///
|
|
||||||
/// Fast multiplication in gamma-space.
|
|
||||||
///
|
|
||||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
|
||||||
#[inline]
|
|
||||||
pub fn gamma_multiply(self, factor: f32) -> Color32 {
|
|
||||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
|
||||||
let Self([r, g, b, a]) = self;
|
|
||||||
Self([
|
|
||||||
(r as f32 * factor + 0.5) as u8,
|
|
||||||
(g as f32 * factor + 0.5) as u8,
|
|
||||||
(b as f32 * factor + 0.5) as u8,
|
|
||||||
(a as f32 * factor + 0.5) as u8,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply with 0.5 to make color half as opaque in linear space.
|
|
||||||
///
|
|
||||||
/// This is using linear space, which is not perceptually even.
|
|
||||||
/// You may want to use [`Self::gamma_multiply`] instead.
|
|
||||||
pub fn linear_multiply(self, factor: f32) -> Color32 {
|
|
||||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
|
||||||
// As an unfortunate side-effect of using premultiplied alpha
|
|
||||||
// we need a somewhat expensive conversion to linear space and back.
|
|
||||||
Rgba::from(self).multiply(factor).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
|
|
||||||
///
|
|
||||||
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
|
|
||||||
/// in order to obtain linear space color values.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
|
|
||||||
let Self([r, g, b, a]) = self;
|
|
||||||
[
|
|
||||||
r as f32 / 255.0,
|
|
||||||
g as f32 / 255.0,
|
|
||||||
b as f32 / 255.0,
|
|
||||||
a as f32 / 255.0,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
/// Construct a [`crate::Color32`] from a hex RGB or RGBA string.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use ecolor::{hex_color, Color32};
|
|
||||||
/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22));
|
|
||||||
/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12));
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! hex_color {
|
|
||||||
($s:literal) => {{
|
|
||||||
let array = color_hex::color_from_hex!($s);
|
|
||||||
if array.len() == 3 {
|
|
||||||
$crate::Color32::from_rgb(array[0], array[1], array[2])
|
|
||||||
} else {
|
|
||||||
#[allow(unconditional_panic)]
|
|
||||||
$crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3])
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from_rgb_hex() {
|
|
||||||
assert_eq!(
|
|
||||||
crate::Color32::from_rgb(0x20, 0x21, 0x22),
|
|
||||||
hex_color!("#202122")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
crate::Color32::from_rgb_additive(0x20, 0x21, 0x22),
|
|
||||||
hex_color!("#202122").additive()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from_rgba_hex() {
|
|
||||||
assert_eq!(
|
|
||||||
crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50),
|
|
||||||
hex_color!("20212250")
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,231 +0,0 @@
|
||||||
use crate::{
|
|
||||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
|
||||||
linear_u8_from_linear_f32, Color32, Rgba,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
|
||||||
/// No premultiplied alpha.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
||||||
pub struct Hsva {
|
|
||||||
/// hue 0-1
|
|
||||||
pub h: f32,
|
|
||||||
|
|
||||||
/// saturation 0-1
|
|
||||||
pub s: f32,
|
|
||||||
|
|
||||||
/// value 0-1
|
|
||||||
pub v: f32,
|
|
||||||
|
|
||||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
|
||||||
pub a: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hsva {
|
|
||||||
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
|
|
||||||
Self { h, s, v, a }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From `sRGBA` with premultiplied alpha
|
|
||||||
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
|
|
||||||
Self::from_rgba_premultiplied(
|
|
||||||
linear_f32_from_gamma_u8(srgba[0]),
|
|
||||||
linear_f32_from_gamma_u8(srgba[1]),
|
|
||||||
linear_f32_from_gamma_u8(srgba[2]),
|
|
||||||
linear_f32_from_linear_u8(srgba[3]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From `sRGBA` without premultiplied alpha
|
|
||||||
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
|
|
||||||
Self::from_rgba_unmultiplied(
|
|
||||||
linear_f32_from_gamma_u8(srgba[0]),
|
|
||||||
linear_f32_from_gamma_u8(srgba[1]),
|
|
||||||
linear_f32_from_gamma_u8(srgba[2]),
|
|
||||||
linear_f32_from_linear_u8(srgba[3]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From linear RGBA with premultiplied alpha
|
|
||||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
|
||||||
#![allow(clippy::many_single_char_names)]
|
|
||||||
if a == 0.0 {
|
|
||||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
|
||||||
Hsva::default()
|
|
||||||
} else {
|
|
||||||
Hsva::from_additive_rgb([r, g, b])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
|
|
||||||
Hsva { h, s, v, a }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From linear RGBA without premultiplied alpha
|
|
||||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
|
||||||
#![allow(clippy::many_single_char_names)]
|
|
||||||
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
|
||||||
Hsva { h, s, v, a }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
|
|
||||||
let (h, s, v) = hsv_from_rgb(rgb);
|
|
||||||
Hsva {
|
|
||||||
h,
|
|
||||||
s,
|
|
||||||
v,
|
|
||||||
a: -0.5, // anything negative is treated as additive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_rgb(rgb: [f32; 3]) -> Self {
|
|
||||||
let (h, s, v) = hsv_from_rgb(rgb);
|
|
||||||
Hsva { h, s, v, a: 1.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
|
|
||||||
Self::from_rgb([
|
|
||||||
linear_f32_from_gamma_u8(r),
|
|
||||||
linear_f32_from_gamma_u8(g),
|
|
||||||
linear_f32_from_gamma_u8(b),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
pub fn to_opaque(self) -> Self {
|
|
||||||
Self { a: 1.0, ..self }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_rgb(&self) -> [f32; 3] {
|
|
||||||
rgb_from_hsv((self.h, self.s, self.v))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_srgb(&self) -> [u8; 3] {
|
|
||||||
let [r, g, b] = self.to_rgb();
|
|
||||||
[
|
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
|
||||||
gamma_u8_from_linear_f32(b),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
|
|
||||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
|
||||||
let additive = a < 0.0;
|
|
||||||
if additive {
|
|
||||||
[r, g, b, 0.0]
|
|
||||||
} else {
|
|
||||||
[a * r, a * g, a * b, a]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents additive colors using a negative alpha.
|
|
||||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
|
||||||
let Hsva { h, s, v, a } = *self;
|
|
||||||
let [r, g, b] = rgb_from_hsv((h, s, v));
|
|
||||||
[r, g, b, a]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
|
||||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
|
||||||
[
|
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
|
||||||
gamma_u8_from_linear_f32(b),
|
|
||||||
linear_u8_from_linear_f32(a),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
|
||||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
|
||||||
[
|
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
|
||||||
gamma_u8_from_linear_f32(b),
|
|
||||||
linear_u8_from_linear_f32(a.abs()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Hsva> for Rgba {
|
|
||||||
fn from(hsva: Hsva) -> Rgba {
|
|
||||||
Rgba(hsva.to_rgba_premultiplied())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rgba> for Hsva {
|
|
||||||
fn from(rgba: Rgba) -> Hsva {
|
|
||||||
Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Hsva> for Color32 {
|
|
||||||
fn from(hsva: Hsva) -> Color32 {
|
|
||||||
Color32::from(Rgba::from(hsva))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color32> for Hsva {
|
|
||||||
fn from(srgba: Color32) -> Hsva {
|
|
||||||
Hsva::from(Rgba::from(srgba))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All ranges in 0-1, rgb is linear.
|
|
||||||
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
|
||||||
#![allow(clippy::many_single_char_names)]
|
|
||||||
let min = r.min(g.min(b));
|
|
||||||
let max = r.max(g.max(b)); // value
|
|
||||||
|
|
||||||
let range = max - min;
|
|
||||||
|
|
||||||
let h = if max == min {
|
|
||||||
0.0 // hue is undefined
|
|
||||||
} else if max == r {
|
|
||||||
(g - b) / (6.0 * range)
|
|
||||||
} else if max == g {
|
|
||||||
(b - r) / (6.0 * range) + 1.0 / 3.0
|
|
||||||
} else {
|
|
||||||
// max == b
|
|
||||||
(r - g) / (6.0 * range) + 2.0 / 3.0
|
|
||||||
};
|
|
||||||
let h = (h + 1.0).fract(); // wrap
|
|
||||||
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
|
|
||||||
(h, s, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All ranges in 0-1, rgb is linear.
|
|
||||||
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
|
||||||
#![allow(clippy::many_single_char_names)]
|
|
||||||
let h = (h.fract() + 1.0).fract(); // wrap
|
|
||||||
let s = s.clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
let f = h * 6.0 - (h * 6.0).floor();
|
|
||||||
let p = v * (1.0 - s);
|
|
||||||
let q = v * (1.0 - f * s);
|
|
||||||
let t = v * (1.0 - (1.0 - f) * s);
|
|
||||||
|
|
||||||
match (h * 6.0).floor() as i32 % 6 {
|
|
||||||
0 => [v, t, p],
|
|
||||||
1 => [q, v, p],
|
|
||||||
2 => [p, v, t],
|
|
||||||
3 => [p, q, v],
|
|
||||||
4 => [t, p, v],
|
|
||||||
5 => [v, p, q],
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore] // a bit expensive
|
|
||||||
fn test_hsv_roundtrip() {
|
|
||||||
for r in 0..=255 {
|
|
||||||
for g in 0..=255 {
|
|
||||||
for b in 0..=255 {
|
|
||||||
let srgba = Color32::from_rgb(r, g, b);
|
|
||||||
let hsva = Hsva::from(srgba);
|
|
||||||
assert_eq!(srgba, Color32::from(hsva));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
|
|
||||||
|
|
||||||
/// Like Hsva but with the `v` value (brightness) being gamma corrected
|
|
||||||
/// so that it is somewhat perceptually even.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
||||||
pub struct HsvaGamma {
|
|
||||||
/// hue 0-1
|
|
||||||
pub h: f32,
|
|
||||||
|
|
||||||
/// saturation 0-1
|
|
||||||
pub s: f32,
|
|
||||||
|
|
||||||
/// value 0-1, in gamma-space (~perceptually even)
|
|
||||||
pub v: f32,
|
|
||||||
|
|
||||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
|
||||||
pub a: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HsvaGamma> for Rgba {
|
|
||||||
fn from(hsvag: HsvaGamma) -> Rgba {
|
|
||||||
Hsva::from(hsvag).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HsvaGamma> for Color32 {
|
|
||||||
fn from(hsvag: HsvaGamma) -> Color32 {
|
|
||||||
Rgba::from(hsvag).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HsvaGamma> for Hsva {
|
|
||||||
fn from(hsvag: HsvaGamma) -> Hsva {
|
|
||||||
let HsvaGamma { h, s, v, a } = hsvag;
|
|
||||||
Hsva {
|
|
||||||
h,
|
|
||||||
s,
|
|
||||||
v: linear_from_gamma(v),
|
|
||||||
a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rgba> for HsvaGamma {
|
|
||||||
fn from(rgba: Rgba) -> HsvaGamma {
|
|
||||||
Hsva::from(rgba).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color32> for HsvaGamma {
|
|
||||||
fn from(srgba: Color32) -> HsvaGamma {
|
|
||||||
Hsva::from(srgba).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Hsva> for HsvaGamma {
|
|
||||||
fn from(hsva: Hsva) -> HsvaGamma {
|
|
||||||
let Hsva { h, s, v, a } = hsva;
|
|
||||||
HsvaGamma {
|
|
||||||
h,
|
|
||||||
s,
|
|
||||||
v: gamma_from_linear(v),
|
|
||||||
a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
//! Color conversions and types.
|
|
||||||
//!
|
|
||||||
//! If you want a compact color representation, use [`Color32`].
|
|
||||||
//! If you want to manipulate RGBA colors use [`Rgba`].
|
|
||||||
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
|
|
||||||
//!
|
|
||||||
//! ## Feature flags
|
|
||||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
|
||||||
//!
|
|
||||||
|
|
||||||
#![allow(clippy::wrong_self_convention)]
|
|
||||||
|
|
||||||
#[cfg(feature = "cint")]
|
|
||||||
mod cint_impl;
|
|
||||||
#[cfg(feature = "cint")]
|
|
||||||
pub use cint_impl::*;
|
|
||||||
|
|
||||||
mod color32;
|
|
||||||
pub use color32::*;
|
|
||||||
|
|
||||||
mod hsva_gamma;
|
|
||||||
pub use hsva_gamma::*;
|
|
||||||
|
|
||||||
mod hsva;
|
|
||||||
pub use hsva::*;
|
|
||||||
|
|
||||||
#[cfg(feature = "color-hex")]
|
|
||||||
mod hex_color_macro;
|
|
||||||
|
|
||||||
mod rgba;
|
|
||||||
pub use rgba::*;
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Color conversion:
|
|
||||||
|
|
||||||
impl From<Color32> for Rgba {
|
|
||||||
fn from(srgba: Color32) -> Rgba {
|
|
||||||
Rgba([
|
|
||||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
|
||||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
|
||||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
|
||||||
linear_f32_from_linear_u8(srgba.0[3]),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rgba> for Color32 {
|
|
||||||
fn from(rgba: Rgba) -> Color32 {
|
|
||||||
Color32([
|
|
||||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
|
||||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
|
||||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
|
||||||
linear_u8_from_linear_f32(rgba.0[3]),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gamma [0, 255] -> linear [0, 1].
|
|
||||||
pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
|
|
||||||
if s <= 10 {
|
|
||||||
s as f32 / 3294.6
|
|
||||||
} else {
|
|
||||||
((s as f32 + 14.025) / 269.025).powf(2.4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// linear [0, 255] -> linear [0, 1].
|
|
||||||
/// Useful for alpha-channel.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
|
|
||||||
a as f32 / 255.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// linear [0, 1] -> gamma [0, 255] (clamped).
|
|
||||||
/// Values outside this range will be clamped to the range.
|
|
||||||
pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
|
|
||||||
if l <= 0.0 {
|
|
||||||
0
|
|
||||||
} else if l <= 0.0031308 {
|
|
||||||
fast_round(3294.6 * l)
|
|
||||||
} else if l <= 1.0 {
|
|
||||||
fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
|
|
||||||
} else {
|
|
||||||
255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// linear [0, 1] -> linear [0, 255] (clamped).
|
|
||||||
/// Useful for alpha-channel.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
|
|
||||||
fast_round(a * 255.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fast_round(r: f32) -> u8 {
|
|
||||||
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_srgba_conversion() {
|
|
||||||
for b in 0..=255 {
|
|
||||||
let l = linear_f32_from_gamma_u8(b);
|
|
||||||
assert!(0.0 <= l && l <= 1.0);
|
|
||||||
assert_eq!(gamma_u8_from_linear_f32(l), b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gamma [0, 1] -> linear [0, 1] (not clamped).
|
|
||||||
/// Works for numbers outside this range (e.g. negative numbers).
|
|
||||||
pub fn linear_from_gamma(gamma: f32) -> f32 {
|
|
||||||
if gamma < 0.0 {
|
|
||||||
-linear_from_gamma(-gamma)
|
|
||||||
} else if gamma <= 0.04045 {
|
|
||||||
gamma / 12.92
|
|
||||||
} else {
|
|
||||||
((gamma + 0.055) / 1.055).powf(2.4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// linear [0, 1] -> gamma [0, 1] (not clamped).
|
|
||||||
/// Works for numbers outside this range (e.g. negative numbers).
|
|
||||||
pub fn gamma_from_linear(linear: f32) -> f32 {
|
|
||||||
if linear < 0.0 {
|
|
||||||
-gamma_from_linear(-linear)
|
|
||||||
} else if linear <= 0.0031308 {
|
|
||||||
12.92 * linear
|
|
||||||
} else {
|
|
||||||
1.055 * linear.powf(1.0 / 2.4) - 0.055
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
|
|
||||||
/// or with the `extra_debug_asserts` feature in debug builds.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! ecolor_assert {
|
|
||||||
($($arg: tt)*) => {
|
|
||||||
if cfg!(any(
|
|
||||||
feature = "extra_asserts",
|
|
||||||
all(feature = "extra_debug_asserts", debug_assertions),
|
|
||||||
)) {
|
|
||||||
assert!($($arg)*);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Cheap and ugly.
|
|
||||||
/// Made for graying out disabled `Ui`s.
|
|
||||||
pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
|
|
||||||
let [mut r, mut g, mut b, mut a] = color.to_array();
|
|
||||||
|
|
||||||
if a == 0 {
|
|
||||||
r /= 2;
|
|
||||||
g /= 2;
|
|
||||||
b /= 2;
|
|
||||||
} else if a < 170 {
|
|
||||||
// Cheapish and looks ok.
|
|
||||||
// Works for e.g. grid stripes.
|
|
||||||
let div = (2 * 255 / a as i32) as u8;
|
|
||||||
r = r / 2 + target.r() / div;
|
|
||||||
g = g / 2 + target.g() / div;
|
|
||||||
b = b / 2 + target.b() / div;
|
|
||||||
a /= 2;
|
|
||||||
} else {
|
|
||||||
r = r / 2 + target.r() / 2;
|
|
||||||
g = g / 2 + target.g() / 2;
|
|
||||||
b = b / 2 + target.b() / 2;
|
|
||||||
}
|
|
||||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
use crate::{
|
|
||||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
|
||||||
linear_u8_from_linear_f32,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
|
||||||
pub struct Rgba(pub(crate) [f32; 4]);
|
|
||||||
|
|
||||||
impl std::ops::Index<usize> for Rgba {
|
|
||||||
type Output = f32;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn index(&self, index: usize) -> &f32 {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::IndexMut<usize> for Rgba {
|
|
||||||
#[inline(always)]
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
|
||||||
if f == 0.0 {
|
|
||||||
state.write_u8(0);
|
|
||||||
} else if f.is_nan() {
|
|
||||||
state.write_u8(1);
|
|
||||||
} else {
|
|
||||||
use std::hash::Hash;
|
|
||||||
f.to_bits().hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::derive_hash_xor_eq)]
|
|
||||||
impl std::hash::Hash for Rgba {
|
|
||||||
#[inline]
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
crate::f32_hash(state, self.0[0]);
|
|
||||||
crate::f32_hash(state, self.0[1]);
|
|
||||||
crate::f32_hash(state, self.0[2]);
|
|
||||||
crate::f32_hash(state, self.0[3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rgba {
|
|
||||||
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
|
|
||||||
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
|
|
||||||
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
|
|
||||||
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
|
|
||||||
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
|
|
||||||
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
|
||||||
Self([r, g, b, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
|
||||||
Self([r * a, g * a, b * a, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
let r = linear_f32_from_gamma_u8(r);
|
|
||||||
let g = linear_f32_from_gamma_u8(g);
|
|
||||||
let b = linear_f32_from_gamma_u8(b);
|
|
||||||
let a = linear_f32_from_linear_u8(a);
|
|
||||||
Self::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
let r = linear_f32_from_gamma_u8(r);
|
|
||||||
let g = linear_f32_from_gamma_u8(g);
|
|
||||||
let b = linear_f32_from_gamma_u8(b);
|
|
||||||
let a = linear_f32_from_linear_u8(a);
|
|
||||||
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
|
|
||||||
Self([r, g, b, 1.0])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn from_gray(l: f32) -> Self {
|
|
||||||
Self([l, l, l, 1.0])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
|
||||||
crate::ecolor_assert!(0.0 <= l && l <= 1.0);
|
|
||||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
|
||||||
Self([l * a, l * a, l * a, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transparent black
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_black_alpha(a: f32) -> Self {
|
|
||||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
|
||||||
Self([0.0, 0.0, 0.0, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transparent white
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_white_alpha(a: f32) -> Self {
|
|
||||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
|
|
||||||
Self([a, a, a, a])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return an additive version of this color (alpha = 0)
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn additive(self) -> Self {
|
|
||||||
let [r, g, b, _] = self.0;
|
|
||||||
Self([r, g, b, 0.0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply with e.g. 0.5 to make us half transparent
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn multiply(self, alpha: f32) -> Self {
|
|
||||||
Self([
|
|
||||||
alpha * self[0],
|
|
||||||
alpha * self[1],
|
|
||||||
alpha * self[2],
|
|
||||||
alpha * self[3],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn r(&self) -> f32 {
|
|
||||||
self.0[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn g(&self) -> f32 {
|
|
||||||
self.0[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn b(&self) -> f32 {
|
|
||||||
self.0[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn a(&self) -> f32 {
|
|
||||||
self.0[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How perceptually intense (bright) is the color?
|
|
||||||
#[inline]
|
|
||||||
pub fn intensity(&self) -> f32 {
|
|
||||||
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an opaque version of self
|
|
||||||
pub fn to_opaque(&self) -> Self {
|
|
||||||
if self.a() == 0.0 {
|
|
||||||
// Additive or fully transparent black.
|
|
||||||
Self::from_rgb(self.r(), self.g(), self.b())
|
|
||||||
} else {
|
|
||||||
// un-multiply alpha:
|
|
||||||
Self::from_rgb(
|
|
||||||
self.r() / self.a(),
|
|
||||||
self.g() / self.a(),
|
|
||||||
self.b() / self.a(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Premultiplied RGBA
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn to_array(&self) -> [f32; 4] {
|
|
||||||
[self.r(), self.g(), self.b(), self.a()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Premultiplied RGBA
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
|
|
||||||
(self.r(), self.g(), self.b(), self.a())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// unmultiply the alpha
|
|
||||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
|
||||||
let a = self.a();
|
|
||||||
if a == 0.0 {
|
|
||||||
// Additive, let's assume we are black
|
|
||||||
self.0
|
|
||||||
} else {
|
|
||||||
[self.r() / a, self.g() / a, self.b() / a, a]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// unmultiply the alpha
|
|
||||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
|
||||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
|
||||||
[
|
|
||||||
gamma_u8_from_linear_f32(r),
|
|
||||||
gamma_u8_from_linear_f32(g),
|
|
||||||
gamma_u8_from_linear_f32(b),
|
|
||||||
linear_u8_from_linear_f32(a.abs()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Add for Rgba {
|
|
||||||
type Output = Rgba;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn add(self, rhs: Rgba) -> Rgba {
|
|
||||||
Rgba([
|
|
||||||
self[0] + rhs[0],
|
|
||||||
self[1] + rhs[1],
|
|
||||||
self[2] + rhs[2],
|
|
||||||
self[3] + rhs[3],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Mul<Rgba> for Rgba {
|
|
||||||
type Output = Rgba;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn mul(self, other: Rgba) -> Rgba {
|
|
||||||
Rgba([
|
|
||||||
self[0] * other[0],
|
|
||||||
self[1] * other[1],
|
|
||||||
self[2] * other[2],
|
|
||||||
self[3] * other[3],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Mul<f32> for Rgba {
|
|
||||||
type Output = Rgba;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn mul(self, factor: f32) -> Rgba {
|
|
||||||
Rgba([
|
|
||||||
self[0] * factor,
|
|
||||||
self[1] * factor,
|
|
||||||
self[2] * factor,
|
|
||||||
self[3] * factor,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Mul<Rgba> for f32 {
|
|
||||||
type Output = Rgba;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn mul(self, rgba: Rgba) -> Rgba {
|
|
||||||
Rgba([
|
|
||||||
self * rgba[0],
|
|
||||||
self * rgba[1],
|
|
||||||
self * rgba[2],
|
|
||||||
self * rgba[3],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,64 +5,19 @@ 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:
|
|
||||||
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
|
|
||||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
|
||||||
* Fix bug where the cursor could get stuck using the wrong icon.
|
|
||||||
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
|
|
||||||
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
|
|
||||||
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
|
|
||||||
|
|
||||||
#### Web:
|
|
||||||
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.1 - 2022-12-11
|
|
||||||
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
|
|
||||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||||
* Allow empty textures with the glow renderer.
|
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
||||||
|
|
||||||
#### Desktop/Native:
|
|
||||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
|
||||||
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
|
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||||
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
|
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||||
|
* Allow empty textures with the glow renderer.
|
||||||
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
|
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
|
||||||
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
|
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
|
||||||
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
|
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
|
||||||
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
|
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs`.
|
||||||
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
|
|
||||||
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
|
||||||
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
|
||||||
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
|
||||||
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
|
||||||
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
|
|
||||||
|
|
||||||
#### Web:
|
|
||||||
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
|
|
||||||
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||||
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
|
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
||||||
|
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
||||||
|
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
|
@ -154,7 +109,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||||
|
|
||||||
|
|
||||||
## 0.15.0 - 2021-10-24
|
## 0.15.0 - 2021-10-24
|
||||||
* `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
|
* `Frame` now provides `set_window_title` to set window title dynamically
|
||||||
* `Frame` now provides `set_decorations` to set whether to show window decorations.
|
* `Frame` now provides `set_decorations` to set whether to show window decorations.
|
||||||
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
||||||
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "eframe"
|
name = "eframe"
|
||||||
version = "0.21.3"
|
version = "0.19.0"
|
||||||
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"
|
||||||
|
@ -15,16 +15,12 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["accesskit", "default_fonts", "glow"]
|
default = ["default_fonts", "glow"]
|
||||||
|
|
||||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
|
||||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
|
||||||
|
|
||||||
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
||||||
##
|
##
|
||||||
|
@ -36,7 +32,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", "dep:glutin-winit"]
|
glow = ["dep:glow", "egui_glow"]
|
||||||
|
|
||||||
## Enable saving app state to disk.
|
## Enable saving app state to disk.
|
||||||
persistence = [
|
persistence = [
|
||||||
|
@ -53,64 +49,48 @@ 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_mut(|o| o.screen_reader = true);`) on web.
|
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
|
||||||
##
|
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.
|
|
||||||
## This is used to generate images for the examples.
|
|
||||||
__screenshot = ["dep:image"]
|
|
||||||
|
|
||||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
||||||
## This overrides the `glow` feature.
|
## This overrides the `glow` feature.
|
||||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
wgpu = ["dep:wgpu", "dep:egui-wgpu"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
egui = { version = "0.19.0", path = "../egui", default-features = false, features = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"tracing",
|
"tracing",
|
||||||
] }
|
] }
|
||||||
thiserror = "1.0.37"
|
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
#! ### 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 }
|
||||||
|
|
||||||
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
|
egui_glow = { version = "0.19.0", path = "../egui_glow", optional = true, default-features = false }
|
||||||
glow = { version = "0.12", optional = true }
|
glow = { version = "0.11", 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.21.1", path = "../egui-winit", default-features = false, features = [
|
egui-winit = { version = "0.19.0", path = "../egui-winit", default-features = false, features = [
|
||||||
"clipboard",
|
"clipboard",
|
||||||
"links",
|
"links",
|
||||||
] }
|
] }
|
||||||
raw-window-handle = { version = "0.5.0" }
|
glutin = { version = "0.29.0" }
|
||||||
winit = "0.28.1"
|
winit = "0.27.2"
|
||||||
|
|
||||||
# optional native:
|
# optional native:
|
||||||
dark-light = { version = "1.0", optional = true }
|
dark-light = { version = "0.2.1", optional = true }
|
||||||
directories-next = { version = "2", optional = true }
|
directories-next = { version = "2", optional = true }
|
||||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
|
egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true, features = [
|
||||||
"winit",
|
"winit",
|
||||||
] } # if wgpu is used, use it with winit
|
|
||||||
pollster = { version = "0.3", optional = true } # needed for wgpu
|
|
||||||
|
|
||||||
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
|
|
||||||
# this can be done at the same time we expose x11/wayland features of winit crate.
|
|
||||||
glutin = { version = "0.30", optional = true }
|
|
||||||
glutin-winit = { version = "0.3.0", optional = true }
|
|
||||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
|
||||||
"png",
|
|
||||||
] }
|
] }
|
||||||
puffin = { version = "0.14", optional = true }
|
puffin = { version = "0.14", optional = true }
|
||||||
wgpu = { version = "0.15.0", optional = true }
|
wgpu = { version = "0.14", optional = true }
|
||||||
|
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# web:
|
# web:
|
||||||
|
@ -118,7 +98,7 @@ wgpu = { version = "0.15.0", 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.84"
|
wasm-bindgen = "0.2"
|
||||||
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",
|
||||||
|
@ -164,6 +144,6 @@ web-sys = { version = "0.3.58", features = [
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# optional web:
|
# optional web:
|
||||||
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||||
tts = { version = "0.25", optional = true, default-features = false }
|
tts = { version = "0.24", optional = true }
|
||||||
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }
|
wgpu = { version = "0.14", 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 libxkbcommon-dev libssl-dev
|
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
You 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.
|
||||||
|
@ -45,6 +45,7 @@ You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/win
|
||||||
* Mobile text editing is not as good as for a normal web app.
|
* Mobile text editing is not as good as for a normal web app.
|
||||||
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
|
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
|
||||||
* No integration with browser settings for colors and fonts.
|
* No integration with browser settings for colors and fonts.
|
||||||
|
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
|
||||||
|
|
||||||
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
|
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,9 @@
|
||||||
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
|
||||||
|
@ -22,7 +20,6 @@ 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.
|
||||||
|
@ -77,7 +74,7 @@ pub trait App {
|
||||||
///
|
///
|
||||||
/// Can be used from web to interact or other external context.
|
/// Can be used from web to interact or other external context.
|
||||||
///
|
///
|
||||||
/// You need to implement this if you want to be able to access the application from JS using [`crate::web::backend::AppRunner`].
|
/// You need to implement this if you want to be able to access the application from JS using [`AppRunner::app_mut`].
|
||||||
///
|
///
|
||||||
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
|
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
|
||||||
///
|
///
|
||||||
|
@ -148,25 +145,20 @@ pub trait App {
|
||||||
/// The size limit of the web app canvas.
|
/// The size limit of the web app canvas.
|
||||||
///
|
///
|
||||||
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
|
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
|
||||||
|
///
|
||||||
|
/// A large canvas can lead to bad frame rates on some older browsers on some platforms
|
||||||
|
/// (see <https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0>).
|
||||||
fn max_size_points(&self) -> egui::Vec2 {
|
fn max_size_points(&self) -> egui::Vec2 {
|
||||||
egui::Vec2::INFINITY
|
egui::Vec2::INFINITY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
|
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
|
||||||
///
|
|
||||||
/// This is the background of your windows if you don't set a central panel.
|
/// 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).to_normalized_gamma_f32()
|
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
|
||||||
|
|
||||||
// _visuals.window_fill() would also be a natural choice
|
// _visuals.window_fill() would also be a natural choice
|
||||||
}
|
}
|
||||||
|
@ -184,7 +176,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(|mem| mem.everything_is_visible())` will be set to `true`.
|
/// `ctx.memory().everything_is_visible()` will be set to `true`.
|
||||||
///
|
///
|
||||||
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
|
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
|
||||||
///
|
///
|
||||||
|
@ -266,10 +258,10 @@ pub struct NativeOptions {
|
||||||
/// The initial inner size of the native window in points (logical pixels).
|
/// The initial inner size of the native window in points (logical pixels).
|
||||||
pub initial_window_size: Option<egui::Vec2>,
|
pub initial_window_size: Option<egui::Vec2>,
|
||||||
|
|
||||||
/// The minimum inner window size in points (logical pixels).
|
/// The minimum inner window size
|
||||||
pub min_window_size: Option<egui::Vec2>,
|
pub min_window_size: Option<egui::Vec2>,
|
||||||
|
|
||||||
/// The maximum inner window size in points (logical pixels).
|
/// The maximum inner window size
|
||||||
pub max_window_size: Option<egui::Vec2>,
|
pub max_window_size: Option<egui::Vec2>,
|
||||||
|
|
||||||
/// Should the app window be resizable?
|
/// Should the app window be resizable?
|
||||||
|
@ -320,7 +312,6 @@ 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:
|
||||||
|
@ -359,7 +350,6 @@ 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")]
|
||||||
|
@ -386,13 +376,9 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,10 +392,8 @@ 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,
|
||||||
|
@ -424,22 +408,14 @@ 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(),
|
||||||
}
|
}
|
||||||
|
@ -456,7 +432,6 @@ 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
|
||||||
|
@ -578,7 +553,6 @@ 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"))]
|
||||||
|
@ -592,7 +566,6 @@ 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")]
|
||||||
|
@ -608,7 +581,6 @@ 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 {
|
||||||
|
@ -621,7 +593,6 @@ 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;
|
||||||
|
|
||||||
|
@ -737,22 +708,9 @@ impl Frame {
|
||||||
#[doc(alias = "exit")]
|
#[doc(alias = "exit")]
|
||||||
#[doc(alias = "quit")]
|
#[doc(alias = "quit")]
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
tracing::debug!("eframe::Frame::close called");
|
|
||||||
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`"]
|
||||||
|
@ -764,7 +722,6 @@ impl Frame {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
||||||
self.output.window_size = Some(size);
|
self.output.window_size = Some(size);
|
||||||
self.info.window_info.size = size; // so that subsequent calls see the updated value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the desired title of the window.
|
/// Set the desired title of the window.
|
||||||
|
@ -785,14 +742,12 @@ impl Frame {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||||
self.output.fullscreen = Some(fullscreen);
|
self.output.fullscreen = Some(fullscreen);
|
||||||
self.info.window_info.fullscreen = fullscreen; // so that subsequent calls see the updated value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// set the position of the outer window.
|
/// set the position of the outer window.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
||||||
self.output.window_pos = Some(pos);
|
self.output.window_pos = Some(pos);
|
||||||
self.info.window_info.position = Some(pos); // so that subsequent calls see the updated value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When called, the native window will follow the
|
/// When called, the native window will follow the
|
||||||
|
@ -834,7 +789,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -865,12 +819,6 @@ 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,
|
||||||
|
|
||||||
|
@ -1053,13 +1001,5 @@ 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>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,20 +103,9 @@ pub use web_sys;
|
||||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||||
/// #[cfg(target_arch = "wasm32")]
|
/// #[cfg(target_arch = "wasm32")]
|
||||||
/// #[wasm_bindgen]
|
/// #[wasm_bindgen]
|
||||||
/// pub struct WebHandle {
|
/// pub async fn start(canvas_id: &str) -> Result<AppRunnerRef>, eframe::wasm_bindgen::JsValue> {
|
||||||
/// handle: AppRunnerRef,
|
|
||||||
/// }
|
|
||||||
/// #[cfg(target_arch = "wasm32")]
|
|
||||||
/// #[wasm_bindgen]
|
|
||||||
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
|
|
||||||
/// let web_options = eframe::WebOptions::default();
|
/// let web_options = eframe::WebOptions::default();
|
||||||
/// eframe::start_web(
|
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
|
||||||
/// canvas_id,
|
|
||||||
/// web_options,
|
|
||||||
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
|
|
||||||
/// )
|
|
||||||
/// .await
|
|
||||||
/// .map(|handle| WebHandle { handle })
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
@ -127,7 +116,7 @@ pub async fn start_web(
|
||||||
canvas_id: &str,
|
canvas_id: &str,
|
||||||
web_options: WebOptions,
|
web_options: WebOptions,
|
||||||
app_creator: AppCreator,
|
app_creator: AppCreator,
|
||||||
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||||
let handle = web::start(canvas_id, web_options, app_creator).await?;
|
let handle = web::start(canvas_id, web_options, app_creator).await?;
|
||||||
|
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
|
@ -137,7 +126,6 @@ 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.
|
||||||
|
@ -175,88 +163,46 @@ mod native;
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// 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(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
|
||||||
pub fn run_native(
|
|
||||||
app_name: &str,
|
|
||||||
native_options: NativeOptions,
|
|
||||||
app_creator: AppCreator,
|
|
||||||
) -> Result<()> {
|
|
||||||
let renderer = native_options.renderer;
|
let renderer = native_options.renderer;
|
||||||
|
|
||||||
#[cfg(not(feature = "__screenshot"))]
|
|
||||||
assert!(
|
|
||||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
|
||||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
|
||||||
);
|
|
||||||
|
|
||||||
match renderer {
|
match renderer {
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
Renderer::Glow => {
|
Renderer::Glow => {
|
||||||
tracing::debug!("Using the glow renderer");
|
tracing::debug!("Using the glow renderer");
|
||||||
native::run::run_glow(app_name, native_options, app_creator)
|
native::run::run_glow(app_name, native_options, app_creator);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
Renderer::Wgpu => {
|
Renderer::Wgpu => {
|
||||||
tracing::debug!("Using the wgpu renderer");
|
tracing::debug!("Using the wgpu renderer");
|
||||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
native::run::run_wgpu(app_name, native_options, app_creator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// The different problems that can occur when trying to run `eframe`.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
#[error("winit error: {0}")]
|
|
||||||
Winit(#[from] winit::error::OsError),
|
|
||||||
|
|
||||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
|
||||||
#[error("glutin error: {0}")]
|
|
||||||
Glutin(#[from] glutin::error::Error),
|
|
||||||
|
|
||||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
|
||||||
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
|
|
||||||
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
|
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
|
||||||
#[error("WGPU error: {0}")]
|
|
||||||
Wgpu(#[from] egui_wgpu::WgpuError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
|
||||||
mod profiling_scopes {
|
|
||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
macro_rules! profile_function {
|
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"))]
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
pub(crate) use profile_scope;
|
||||||
pub(crate) use profiling_scopes::*;
|
|
||||||
|
|
|
@ -3,23 +3,10 @@ use winit::event_loop::EventLoopWindowTarget;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
use egui::accesskit;
|
|
||||||
use egui::NumExt as _;
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
use egui_winit::accesskit_winit;
|
|
||||||
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
use 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,
|
||||||
|
@ -27,11 +14,7 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_window_info(
|
pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -> WindowInfo {
|
||||||
window: &winit::window::Window,
|
|
||||||
pixels_per_point: f32,
|
|
||||||
window_state: &WindowState,
|
|
||||||
) -> WindowInfo {
|
|
||||||
let position = window
|
let position = window
|
||||||
.outer_position()
|
.outer_position()
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -50,13 +33,9 @@ pub fn read_window_info(
|
||||||
.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,
|
||||||
|
@ -65,13 +44,12 @@ pub fn read_window_info(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn window_builder<E>(
|
pub fn window_builder(
|
||||||
event_loop: &EventLoopWindowTarget<E>,
|
|
||||||
title: &str,
|
|
||||||
native_options: &epi::NativeOptions,
|
native_options: &epi::NativeOptions,
|
||||||
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,
|
||||||
|
@ -85,23 +63,19 @@ pub fn window_builder<E>(
|
||||||
max_window_size,
|
max_window_size,
|
||||||
resizable,
|
resizable,
|
||||||
transparent,
|
transparent,
|
||||||
centered,
|
|
||||||
..
|
..
|
||||||
} = native_options;
|
} = native_options;
|
||||||
|
|
||||||
let window_icon = icon_data.clone().and_then(load_icon);
|
let window_icon = icon_data.clone().and_then(load_icon);
|
||||||
|
|
||||||
let mut window_builder = winit::window::WindowBuilder::new()
|
let mut window_builder = winit::window::WindowBuilder::new()
|
||||||
.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)
|
||||||
.with_resizable(*resizable)
|
.with_resizable(*resizable)
|
||||||
.with_transparent(*transparent)
|
.with_transparent(*transparent)
|
||||||
.with_window_icon(window_icon)
|
.with_window_icon(window_icon);
|
||||||
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
|
||||||
// We must also keep the window hidden until AccessKit is initialized.
|
|
||||||
.with_visible(false);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if *fullsize_content {
|
if *fullsize_content {
|
||||||
|
@ -120,75 +94,23 @@ 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);
|
||||||
|
|
||||||
let inner_size_points = if let Some(mut window_settings) = window_settings {
|
if let Some(window_settings) = window_settings {
|
||||||
// Restore pos/size from previous session
|
|
||||||
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
|
|
||||||
#[cfg(windows)]
|
|
||||||
window_settings.clamp_window_to_sane_position(&event_loop);
|
|
||||||
window_builder = window_settings.initialize_window(window_builder);
|
window_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::LogicalPosition {
|
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
|
||||||
x: pos.x as f64,
|
x: pos.x as f64,
|
||||||
y: pos.y as f64,
|
y: pos.y as f64,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(initial_window_size) = *initial_window_size {
|
if let Some(initial_window_size) = *initial_window_size {
|
||||||
let initial_window_size =
|
|
||||||
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
|
||||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
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 {
|
|
||||||
let mut max_size = egui::Vec2::ZERO;
|
|
||||||
|
|
||||||
for monitor in event_loop.available_monitors() {
|
|
||||||
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
|
|
||||||
let size = egui::vec2(size.width, size.height);
|
|
||||||
max_size = max_size.max(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if max_size == egui::Vec2::ZERO {
|
|
||||||
egui::Vec2::splat(16000.0)
|
|
||||||
} else {
|
|
||||||
max_size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
||||||
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
|
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
|
||||||
}
|
}
|
||||||
|
@ -215,7 +137,6 @@ 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: _,
|
||||||
|
@ -227,8 +148,6 @@ 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 {
|
||||||
|
@ -265,22 +184,7 @@ pub fn handle_app_output(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(always_on_top) = always_on_top {
|
if let Some(always_on_top) = always_on_top {
|
||||||
use winit::window::WindowLevel;
|
window.set_always_on_top(always_on_top);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +211,6 @@ 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 {
|
||||||
|
@ -322,22 +225,16 @@ impl EpiIntegration {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let egui_ctx = egui::Context::default();
|
let egui_ctx = egui::Context::default();
|
||||||
|
|
||||||
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
*egui_ctx.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_state),
|
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
|
||||||
},
|
},
|
||||||
output: epi::backend::AppOutput {
|
output: epi::backend::AppOutput {
|
||||||
visible: Some(true),
|
visible: Some(true),
|
||||||
|
@ -362,37 +259,16 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
|
|
||||||
&mut self,
|
|
||||||
window: &winit::window::Window,
|
|
||||||
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
|
|
||||||
) {
|
|
||||||
let egui_ctx = self.egui_ctx.clone();
|
|
||||||
self.egui_winit
|
|
||||||
.init_accesskit(window, event_loop_proxy, move || {
|
|
||||||
// This function is called when an accessibility client
|
|
||||||
// (e.g. screen reader) makes its first request. If we got here,
|
|
||||||
// we know that an accessibility tree is actually wanted.
|
|
||||||
egui_ctx.enable_accesskit();
|
|
||||||
// Enqueue a repaint so we'll receive a full tree update soon.
|
|
||||||
egui_ctx.request_repaint();
|
|
||||||
egui_ctx.accesskit_placeholder_tree_update()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
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(|mem| mem.clone());
|
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||||
self.egui_ctx
|
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||||
.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_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
|
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||||
self.egui_ctx.clear_animations();
|
self.egui_ctx.clear_animations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,15 +285,8 @@ impl EpiIntegration {
|
||||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => self.close = app.on_close_event(),
|
||||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
WindowEvent::Destroyed => self.close = true,
|
||||||
self.close = app.on_close_event();
|
|
||||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
|
||||||
}
|
|
||||||
WindowEvent::Destroyed => {
|
|
||||||
tracing::debug!("Received WindowEvent::Destroyed");
|
|
||||||
self.close = true;
|
|
||||||
}
|
|
||||||
WindowEvent::MouseInput {
|
WindowEvent::MouseInput {
|
||||||
button: MouseButton::Left,
|
button: MouseButton::Left,
|
||||||
state: ElementState::Pressed,
|
state: ElementState::Pressed,
|
||||||
|
@ -432,11 +301,6 @@ impl EpiIntegration {
|
||||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
|
||||||
self.egui_winit.on_accesskit_action_request(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &mut dyn epi::App,
|
app: &mut dyn epi::App,
|
||||||
|
@ -444,16 +308,12 @@ 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 =
|
self.frame.info.window_info = read_window_info(window, self.egui_ctx.pixels_per_point());
|
||||||
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);
|
||||||
|
|
||||||
|
@ -463,15 +323,9 @@ impl EpiIntegration {
|
||||||
self.can_drag_window = false;
|
self.can_drag_window = false;
|
||||||
if app_output.close {
|
if app_output.close {
|
||||||
self.close = app.on_close_event();
|
self.close = app.on_close_event();
|
||||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
|
||||||
}
|
}
|
||||||
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
||||||
handle_app_output(
|
handle_app_output(window, self.egui_ctx.pixels_per_point(), 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;
|
||||||
|
@ -528,8 +382,7 @@ impl EpiIntegration {
|
||||||
}
|
}
|
||||||
if _app.persist_egui_memory() {
|
if _app.persist_egui_memory() {
|
||||||
crate::profile_scope!("egui_memory");
|
crate::profile_scope!("egui_memory");
|
||||||
self.egui_ctx
|
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
|
||||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
crate::profile_scope!("App::save");
|
crate::profile_scope!("App::save");
|
||||||
|
|
|
@ -26,7 +26,6 @@ impl FileStorage {
|
||||||
/// Store the state in this .ron file.
|
/// Store the state in this .ron file.
|
||||||
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||||
let ron_filepath: PathBuf = ron_filepath.into();
|
let ron_filepath: PathBuf = ron_filepath.into();
|
||||||
tracing::debug!("Loading app state from {:?}…", ron_filepath);
|
|
||||||
Self {
|
Self {
|
||||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||||
ron_filepath,
|
ron_filepath,
|
||||||
|
|
|
@ -1,34 +1,20 @@
|
||||||
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
|
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
|
||||||
//! When making changes to one you often also want to apply it to the other.
|
//! When making changes to one you often also want to apply it to the other.
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use egui_winit::winit;
|
||||||
use winit::event_loop::{
|
use winit::event_loop::{
|
||||||
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
use egui_winit::accesskit_winit;
|
|
||||||
use egui_winit::winit;
|
|
||||||
|
|
||||||
use crate::{epi, Result};
|
|
||||||
|
|
||||||
use super::epi_integration::{self, EpiIntegration};
|
use super::epi_integration::{self, EpiIntegration};
|
||||||
|
use crate::epi;
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UserEvent {
|
pub enum UserEvent {
|
||||||
RequestRepaint,
|
RequestRepaint,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
impl From<accesskit_winit::ActionRequestEvent> for UserEvent {
|
|
||||||
fn from(inner: accesskit_winit::ActionRequestEvent) -> Self {
|
|
||||||
Self::AccessKitActionRequest(inner)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -38,7 +24,6 @@ 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.
|
||||||
|
@ -46,33 +31,25 @@ 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>,
|
||||||
event: &winit::event::Event<'_, UserEvent>,
|
event: &winit::event::Event<'_, UserEvent>,
|
||||||
) -> Result<EventResult>;
|
) -> EventResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_event_loop_builder(
|
fn create_event_loop_builder(
|
||||||
|
@ -91,10 +68,10 @@ fn create_event_loop_builder(
|
||||||
///
|
///
|
||||||
/// We reuse the event-loop so we can support closing and opening an eframe window
|
/// We reuse the event-loop so we can support closing and opening an eframe window
|
||||||
/// multiple times. This is just a limitation of winit.
|
/// multiple times. This is just a limitation of winit.
|
||||||
fn with_event_loop<R>(
|
fn with_event_loop(
|
||||||
mut native_options: epi::NativeOptions,
|
mut native_options: epi::NativeOptions,
|
||||||
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions) -> R,
|
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions),
|
||||||
) -> R {
|
) {
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<UserEvent>>> = RefCell::new(None));
|
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<UserEvent>>> = RefCell::new(None));
|
||||||
|
|
||||||
|
@ -105,31 +82,22 @@ fn with_event_loop<R>(
|
||||||
let mut event_loop = event_loop.borrow_mut();
|
let mut event_loop = event_loop.borrow_mut();
|
||||||
let event_loop = event_loop
|
let event_loop = event_loop
|
||||||
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
|
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
|
||||||
f(event_loop, native_options)
|
f(event_loop, native_options);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_and_return(
|
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl WinitApp) {
|
||||||
event_loop: &mut EventLoop<UserEvent>,
|
|
||||||
mut winit_app: impl WinitApp,
|
|
||||||
) -> Result<()> {
|
|
||||||
use winit::platform::run_return::EventLoopExtRunReturn as _;
|
use winit::platform::run_return::EventLoopExtRunReturn as _;
|
||||||
|
|
||||||
tracing::debug!("Entering the winit event loop (run_return)…");
|
tracing::debug!("event_loop.run_return");
|
||||||
|
|
||||||
let mut next_repaint_time = Instant::now();
|
let mut next_repaint_time = Instant::now();
|
||||||
|
|
||||||
let mut returned_result = Ok(());
|
|
||||||
|
|
||||||
event_loop.run_return(|event, event_loop, control_flow| {
|
event_loop.run_return(|event, event_loop, control_flow| {
|
||||||
let event_result = match &event {
|
let event_result = match &event {
|
||||||
winit::event::Event::LoopDestroyed => {
|
winit::event::Event::LoopDestroyed => {
|
||||||
// On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name),
|
tracing::debug!("winit::event::Event::LoopDestroyed");
|
||||||
// so we need to save state now:
|
EventResult::Exit
|
||||||
tracing::debug!("Received Event::LoopDestroyed - saving app state…");
|
|
||||||
winit_app.save_and_destroy();
|
|
||||||
*control_flow = ControlFlow::Exit;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-dependent event handlers to workaround a winit bug
|
// Platform-dependent event handlers to workaround a winit bug
|
||||||
|
@ -158,28 +126,15 @@ fn run_and_return(
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
|
|
||||||
event => match winit_app.on_event(event_loop, event) {
|
event => winit_app.on_event(event_loop, event),
|
||||||
Ok(event_result) => event_result,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Exiting because of error: {err:?} on event {event:?}");
|
|
||||||
returned_result = Err(err);
|
|
||||||
EventResult::Exit
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match event_result {
|
match event_result {
|
||||||
EventResult::Wait => {}
|
EventResult::Wait => {}
|
||||||
EventResult::RepaintNow => {
|
EventResult::RepaintNow => {
|
||||||
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||||
if cfg!(windows) {
|
|
||||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
|
||||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||||
winit_app.paint();
|
winit_app.paint();
|
||||||
} else {
|
|
||||||
// Fix for https://github.com/emilk/egui/issues/2425
|
|
||||||
next_repaint_time = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EventResult::RepaintNext => {
|
EventResult::RepaintNext => {
|
||||||
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||||
|
@ -189,7 +144,10 @@ fn run_and_return(
|
||||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||||
}
|
}
|
||||||
EventResult::Exit => {
|
EventResult::Exit => {
|
||||||
tracing::debug!("Asking to exit event loop…");
|
// On Cmd-Q we get here and then `run_return` doesn't return,
|
||||||
|
// so we need to save state now:
|
||||||
|
tracing::debug!("Exiting event loop - saving app state…");
|
||||||
|
winit_app.save_and_destroy();
|
||||||
*control_flow = ControlFlow::Exit;
|
*control_flow = ControlFlow::Exit;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -218,21 +176,16 @@ fn run_and_return(
|
||||||
event_loop.run_return(|_, _, control_flow| {
|
event_loop.run_return(|_, _, control_flow| {
|
||||||
control_flow.set_exit();
|
control_flow.set_exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
returned_result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp + 'static) -> ! {
|
fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp + 'static) -> ! {
|
||||||
tracing::debug!("Entering the winit event loop (run)…");
|
tracing::debug!("event_loop.run");
|
||||||
|
|
||||||
let mut next_repaint_time = Instant::now();
|
let mut next_repaint_time = Instant::now();
|
||||||
|
|
||||||
event_loop.run(move |event, event_loop, control_flow| {
|
event_loop.run(move |event, event_loop, control_flow| {
|
||||||
let event_result = match event {
|
let event_result = match event {
|
||||||
winit::event::Event::LoopDestroyed => {
|
winit::event::Event::LoopDestroyed => EventResult::Exit,
|
||||||
tracing::debug!("Received Event::LoopDestroyed");
|
|
||||||
EventResult::Exit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform-dependent event handlers to workaround a winit bug
|
// Platform-dependent event handlers to workaround a winit bug
|
||||||
// See: https://github.com/rust-windowing/winit/issues/987
|
// See: https://github.com/rust-windowing/winit/issues/987
|
||||||
|
@ -251,25 +204,14 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
|
||||||
..
|
..
|
||||||
}) => EventResult::RepaintNext,
|
}) => EventResult::RepaintNext,
|
||||||
|
|
||||||
event => match winit_app.on_event(event_loop, &event) {
|
event => winit_app.on_event(event_loop, &event),
|
||||||
Ok(event_result) => event_result,
|
|
||||||
Err(err) => {
|
|
||||||
panic!("eframe encountered a fatal error: {err}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match event_result {
|
match event_result {
|
||||||
EventResult::Wait => {}
|
EventResult::Wait => {}
|
||||||
EventResult::RepaintNow => {
|
EventResult::RepaintNow => {
|
||||||
if cfg!(windows) {
|
|
||||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
|
||||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||||
winit_app.paint();
|
winit_app.paint();
|
||||||
} else {
|
|
||||||
// Fix for https://github.com/emilk/egui/issues/2425
|
|
||||||
next_repaint_time = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EventResult::RepaintNext => {
|
EventResult::RepaintNext => {
|
||||||
next_repaint_time = Instant::now();
|
next_repaint_time = Instant::now();
|
||||||
|
@ -278,7 +220,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
|
||||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||||
}
|
}
|
||||||
EventResult::Exit => {
|
EventResult::Exit => {
|
||||||
tracing::debug!("Quitting - saving app state…");
|
tracing::debug!("Quitting…");
|
||||||
winit_app.save_and_destroy();
|
winit_app.save_and_destroy();
|
||||||
#[allow(clippy::exit)]
|
#[allow(clippy::exit)]
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
|
@ -299,20 +241,33 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn centere_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")]
|
||||||
mod glow_integration {
|
mod glow_integration {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use egui::NumExt as _;
|
|
||||||
use glutin::{
|
|
||||||
display::GetGlDisplay,
|
|
||||||
prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext},
|
|
||||||
surface::GlSurface,
|
|
||||||
};
|
|
||||||
use raw_window_handle::HasRawWindowHandle;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// Note: that the current Glutin API design tightly couples the GL context with
|
// Note: that the current Glutin API design tightly couples the GL context with
|
||||||
|
@ -336,261 +291,10 @@ 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: glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
builder: winit::window::WindowBuilder,
|
|
||||||
swap_interval: glutin::surface::SwapInterval,
|
|
||||||
gl_config: glutin::config::Config,
|
|
||||||
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 {
|
|
||||||
/// There is a lot of complexity with opengl creation, so prefer extensivve logging to get all the help we can to debug issues.
|
|
||||||
///
|
|
||||||
#[allow(unsafe_code)]
|
|
||||||
unsafe fn new(
|
|
||||||
winit_window_builder: winit::window::WindowBuilder,
|
|
||||||
native_options: &epi::NativeOptions,
|
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
use glutin::prelude::*;
|
|
||||||
// convert native options to glutin options
|
|
||||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
|
||||||
crate::HardwareAcceleration::Required => Some(true),
|
|
||||||
crate::HardwareAcceleration::Preferred => None,
|
|
||||||
crate::HardwareAcceleration::Off => Some(false),
|
|
||||||
};
|
|
||||||
let swap_interval = if native_options.vsync {
|
|
||||||
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
|
|
||||||
} else {
|
|
||||||
glutin::surface::SwapInterval::DontWait
|
|
||||||
};
|
|
||||||
/* opengl setup flow goes like this:
|
|
||||||
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)
|
|
||||||
.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.
|
|
||||||
let config_template_builder = if native_options.multisampling > 0 {
|
|
||||||
config_template_builder.with_multisampling(
|
|
||||||
native_options
|
|
||||||
.multisampling
|
|
||||||
.try_into()
|
|
||||||
.expect("failed to fit multisamples option of native_options into u8"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
config_template_builder
|
|
||||||
};
|
|
||||||
|
|
||||||
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 =
|
|
||||||
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle);
|
|
||||||
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
|
|
||||||
.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 height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
|
|
||||||
let surface_attributes =
|
|
||||||
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
|
|
||||||
.build(window.raw_window_handle(), width, height);
|
|
||||||
tracing::debug!(
|
|
||||||
"creating surface with attributes: {:?}",
|
|
||||||
&surface_attributes
|
|
||||||
);
|
|
||||||
// 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// only applies for android. but we basically drop surface + window and make context not current
|
|
||||||
fn on_suspend(&mut self) -> Result<()> {
|
|
||||||
tracing::debug!("received suspend event. dropping window and surface");
|
|
||||||
self.gl_surface.take();
|
|
||||||
self.window.take();
|
|
||||||
if let Some(current) = self.current_gl_context.take() {
|
|
||||||
tracing::debug!("context is current, so making it non-current");
|
|
||||||
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 {
|
|
||||||
self.window.as_ref().expect("winit window doesn't exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
|
|
||||||
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();
|
|
||||||
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<()> {
|
|
||||||
self.gl_surface
|
|
||||||
.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 {
|
|
||||||
self.gl_config.display().get_proc_address(addr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlowWinitApp {
|
struct GlowWinitApp {
|
||||||
|
@ -604,8 +308,6 @@ mod glow_integration {
|
||||||
// suspends and resumes.
|
// suspends and resumes.
|
||||||
app_creator: Option<epi::AppCreator>,
|
app_creator: Option<epi::AppCreator>,
|
||||||
is_focused: bool,
|
is_focused: bool,
|
||||||
|
|
||||||
frame_nr: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlowWinitApp {
|
impl GlowWinitApp {
|
||||||
|
@ -622,7 +324,6 @@ mod glow_integration {
|
||||||
running: None,
|
running: None,
|
||||||
app_creator: Some(app_creator),
|
app_creator: Some(app_creator),
|
||||||
is_focused: true,
|
is_focused: true,
|
||||||
frame_nr: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,37 +331,47 @@ mod glow_integration {
|
||||||
fn create_glutin_windowed_context(
|
fn create_glutin_windowed_context(
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
storage: Option<&dyn epi::Storage>,
|
storage: Option<&dyn epi::Storage>,
|
||||||
title: &str,
|
title: &String,
|
||||||
native_options: &NativeOptions,
|
native_options: &NativeOptions,
|
||||||
) -> Result<(GlutinWindowContext, glow::Context)> {
|
) -> (
|
||||||
|
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||||
|
glow::Context,
|
||||||
|
) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
|
use crate::HardwareAcceleration;
|
||||||
|
|
||||||
|
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||||
|
HardwareAcceleration::Required => Some(true),
|
||||||
|
HardwareAcceleration::Preferred => None,
|
||||||
|
HardwareAcceleration::Off => Some(false),
|
||||||
|
};
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
|
|
||||||
let winit_window_builder =
|
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
||||||
epi_integration::window_builder(event_loop, title, native_options, window_settings);
|
.with_title(title)
|
||||||
let mut glutin_window_context = unsafe {
|
.with_visible(false); // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
GlutinWindowContext::new(winit_window_builder, native_options, event_loop)?
|
|
||||||
};
|
|
||||||
glutin_window_context.on_resume(event_loop)?;
|
|
||||||
|
|
||||||
if let Some(window) = &glutin_window_context.window {
|
let gl_window = unsafe {
|
||||||
epi_integration::apply_native_options_to_window(window, native_options);
|
glutin::ContextBuilder::new()
|
||||||
}
|
.with_hardware_acceleration(hardware_acceleration)
|
||||||
|
.with_depth_buffer(native_options.depth_buffer)
|
||||||
let gl = unsafe {
|
.with_multisampling(native_options.multisampling)
|
||||||
glow::Context::from_loader_function(|s| {
|
.with_stencil_buffer(native_options.stencil_buffer)
|
||||||
let s = std::ffi::CString::new(s)
|
.with_vsync(native_options.vsync)
|
||||||
.expect("failed to construct C string from string for gl proc address");
|
.build_windowed(window_builder, event_loop)
|
||||||
|
.unwrap()
|
||||||
glutin_window_context.get_proc_address(&s)
|
.make_current()
|
||||||
})
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((glutin_window_context, gl))
|
let gl =
|
||||||
|
unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
|
||||||
|
|
||||||
|
(gl_window, gl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) {
|
||||||
let storage = epi_integration::create_storage(&self.app_name);
|
let storage = epi_integration::create_storage(&self.app_name);
|
||||||
|
|
||||||
let (gl_window, gl) = Self::create_glutin_windowed_context(
|
let (gl_window, gl) = Self::create_glutin_windowed_context(
|
||||||
|
@ -668,7 +379,7 @@ mod glow_integration {
|
||||||
storage.as_deref(),
|
storage.as_deref(),
|
||||||
&self.app_name,
|
&self.app_name,
|
||||||
&self.native_options,
|
&self.native_options,
|
||||||
)?;
|
);
|
||||||
let gl = Arc::new(gl);
|
let gl = Arc::new(gl);
|
||||||
|
|
||||||
let painter =
|
let painter =
|
||||||
|
@ -686,10 +397,6 @@ mod glow_integration {
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone());
|
|
||||||
}
|
|
||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||||
|
|
||||||
|
@ -730,8 +437,6 @@ mod glow_integration {
|
||||||
integration,
|
integration,
|
||||||
app,
|
app,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -812,26 +517,6 @@ mod glow_integration {
|
||||||
|
|
||||||
integration.post_present(window);
|
integration.post_present(window);
|
||||||
|
|
||||||
#[cfg(feature = "__screenshot")]
|
|
||||||
// give it time to settle:
|
|
||||||
if self.frame_nr == 2 {
|
|
||||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
|
||||||
assert!(
|
|
||||||
path.ends_with(".png"),
|
|
||||||
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
|
|
||||||
);
|
|
||||||
let [w, h] = screen_size_in_pixels;
|
|
||||||
let pixels = painter.read_screen_rgba(screen_size_in_pixels);
|
|
||||||
let image = image::RgbaImage::from_vec(w, h, pixels).unwrap();
|
|
||||||
let image = image::imageops::flip_vertical(&image);
|
|
||||||
image.save(&path).unwrap_or_else(|err| {
|
|
||||||
panic!("Failed to save screenshot to {path:?}: {err}");
|
|
||||||
});
|
|
||||||
eprintln!("Screenshot saved to {path:?}.");
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let control_flow = if integration.should_close() {
|
let control_flow = if integration.should_close() {
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
} else if repaint_after.is_zero() {
|
} else if repaint_after.is_zero() {
|
||||||
|
@ -861,8 +546,6 @@ mod glow_integration {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.frame_nr += 1;
|
|
||||||
|
|
||||||
control_flow
|
control_flow
|
||||||
} else {
|
} else {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
|
@ -873,27 +556,29 @@ mod glow_integration {
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
event: &winit::event::Event<'_, UserEvent>,
|
event: &winit::event::Event<'_, UserEvent>,
|
||||||
) -> Result<EventResult> {
|
) -> EventResult {
|
||||||
Ok(match event {
|
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 => {
|
||||||
self.running.as_mut().unwrap().gl_window.on_suspend()?;
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -938,8 +623,7 @@ mod glow_integration {
|
||||||
winit::event::WindowEvent::CloseRequested
|
winit::event::WindowEvent::CloseRequested
|
||||||
if running.integration.should_close() =>
|
if running.integration.should_close() =>
|
||||||
{
|
{
|
||||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
return EventResult::Exit
|
||||||
return Ok(EventResult::Exit);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -962,23 +646,8 @@ mod glow_integration {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
|
||||||
accesskit_winit::ActionRequestEvent { request, .. },
|
|
||||||
)) => {
|
|
||||||
if let Some(running) = &mut self.running {
|
|
||||||
running
|
|
||||||
.integration
|
|
||||||
.on_accesskit_action_request(request.clone());
|
|
||||||
// As a form of user input, accessibility actions should
|
|
||||||
// lead to a repaint.
|
|
||||||
EventResult::RepaintNext
|
|
||||||
} else {
|
|
||||||
EventResult::Wait
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => EventResult::Wait,
|
_ => EventResult::Wait,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -986,15 +655,24 @@ mod glow_integration {
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
mut native_options: epi::NativeOptions,
|
mut native_options: epi::NativeOptions,
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Result<()> {
|
) {
|
||||||
if native_options.run_and_return {
|
if native_options.run_and_return {
|
||||||
with_event_loop(native_options, |event_loop, native_options| {
|
with_event_loop(native_options, |event_loop, mut native_options| {
|
||||||
|
if native_options.centered {
|
||||||
|
centere_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 {
|
||||||
|
centere_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);
|
||||||
}
|
}
|
||||||
|
@ -1040,12 +718,6 @@ mod wgpu_integration {
|
||||||
native_options: epi::NativeOptions,
|
native_options: epi::NativeOptions,
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
#[cfg(feature = "__screenshot")]
|
|
||||||
assert!(
|
|
||||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
|
||||||
"EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend"
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
repaint_proxy: Arc::new(std::sync::Mutex::new(event_loop.create_proxy())),
|
repaint_proxy: Arc::new(std::sync::Mutex::new(event_loop.create_proxy())),
|
||||||
app_name: app_name.to_owned(),
|
app_name: app_name.to_owned(),
|
||||||
|
@ -1060,41 +732,36 @@ mod wgpu_integration {
|
||||||
fn create_window(
|
fn create_window(
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
storage: Option<&dyn epi::Storage>,
|
storage: Option<&dyn epi::Storage>,
|
||||||
title: &str,
|
title: &String,
|
||||||
native_options: &NativeOptions,
|
native_options: &NativeOptions,
|
||||||
) -> std::result::Result<winit::window::Window, winit::error::OsError> {
|
) -> winit::window::Window {
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
let window_builder =
|
epi_integration::window_builder(native_options, &window_settings)
|
||||||
epi_integration::window_builder(event_loop, title, native_options, window_settings);
|
.with_title(title)
|
||||||
let window = window_builder.build(event_loop)?;
|
.with_visible(false) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
epi_integration::apply_native_options_to_window(&window, native_options);
|
.build(event_loop)
|
||||||
Ok(window)
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
fn set_window(
|
fn set_window(&mut self, window: winit::window::Window) {
|
||||||
&mut self,
|
|
||||||
window: winit::window::Window,
|
|
||||||
) -> 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 {
|
||||||
pollster::block_on(running.painter.set_window(self.window.as_ref()))?;
|
running.painter.set_window(self.window.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
fn drop_window(&mut self) {
|
||||||
self.window = None;
|
self.window = None;
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
unsafe {
|
unsafe {
|
||||||
pollster::block_on(running.painter.set_window(None))?;
|
running.painter.set_window(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_run_state(
|
fn init_run_state(
|
||||||
|
@ -1102,16 +769,15 @@ 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<(), 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)))?;
|
painter.set_window(Some(&window));
|
||||||
painter
|
painter
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1128,10 +794,6 @@ mod wgpu_integration {
|
||||||
None,
|
None,
|
||||||
wgpu_render_state.clone(),
|
wgpu_render_state.clone(),
|
||||||
);
|
);
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
integration.init_accesskit(&window, self.repaint_proxy.lock().unwrap().clone());
|
|
||||||
}
|
|
||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||||
|
|
||||||
|
@ -1169,8 +831,6 @@ mod wgpu_integration {
|
||||||
app,
|
app,
|
||||||
});
|
});
|
||||||
self.window = Some(window);
|
self.window = Some(window);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1278,8 +938,8 @@ mod wgpu_integration {
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
event: &winit::event::Event<'_, UserEvent>,
|
event: &winit::event::Event<'_, UserEvent>,
|
||||||
) -> Result<EventResult> {
|
) -> EventResult {
|
||||||
Ok(match event {
|
match event {
|
||||||
winit::event::Event::Resumed => {
|
winit::event::Event::Resumed => {
|
||||||
if let Some(running) = &self.running {
|
if let Some(running) = &self.running {
|
||||||
if self.window.is_none() {
|
if self.window.is_none() {
|
||||||
|
@ -1288,8 +948,8 @@ mod wgpu_integration {
|
||||||
running.integration.frame.storage(),
|
running.integration.frame.storage(),
|
||||||
&self.app_name,
|
&self.app_name,
|
||||||
&self.native_options,
|
&self.native_options,
|
||||||
)?;
|
);
|
||||||
self.set_window(window)?;
|
self.set_window(window);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let storage = epi_integration::create_storage(&self.app_name);
|
let storage = epi_integration::create_storage(&self.app_name);
|
||||||
|
@ -1298,14 +958,14 @@ mod wgpu_integration {
|
||||||
storage.as_deref(),
|
storage.as_deref(),
|
||||||
&self.app_name,
|
&self.app_name,
|
||||||
&self.native_options,
|
&self.native_options,
|
||||||
)?;
|
);
|
||||||
self.init_run_state(event_loop, storage, window)?;
|
self.init_run_state(event_loop, storage, window);
|
||||||
}
|
}
|
||||||
EventResult::RepaintNow
|
EventResult::RepaintNow
|
||||||
}
|
}
|
||||||
winit::event::Event::Suspended => {
|
winit::event::Event::Suspended => {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
self.drop_window()?;
|
self.drop_window();
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1355,8 +1015,7 @@ mod wgpu_integration {
|
||||||
winit::event::WindowEvent::CloseRequested
|
winit::event::WindowEvent::CloseRequested
|
||||||
if running.integration.should_close() =>
|
if running.integration.should_close() =>
|
||||||
{
|
{
|
||||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
return EventResult::Exit
|
||||||
return Ok(EventResult::Exit);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
|
@ -1378,23 +1037,8 @@ mod wgpu_integration {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
|
||||||
accesskit_winit::ActionRequestEvent { request, .. },
|
|
||||||
)) => {
|
|
||||||
if let Some(running) = &mut self.running {
|
|
||||||
running
|
|
||||||
.integration
|
|
||||||
.on_accesskit_action_request(request.clone());
|
|
||||||
// As a form of user input, accessibility actions should
|
|
||||||
// lead to a repaint.
|
|
||||||
EventResult::RepaintNext
|
|
||||||
} else {
|
|
||||||
EventResult::Wait
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => EventResult::Wait,
|
_ => EventResult::Wait,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1402,15 +1046,24 @@ mod wgpu_integration {
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
mut native_options: epi::NativeOptions,
|
mut native_options: epi::NativeOptions,
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Result<()> {
|
) {
|
||||||
if native_options.run_and_return {
|
if native_options.run_and_return {
|
||||||
with_event_loop(native_options, |event_loop, native_options| {
|
with_event_loop(native_options, |event_loop, mut native_options| {
|
||||||
|
if native_options.centered {
|
||||||
|
centere_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 {
|
||||||
|
centere_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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
use super::{web_painter::WebPainter, *};
|
||||||
|
use crate::epi;
|
||||||
|
|
||||||
use egui::{
|
use egui::{
|
||||||
mutex::{Mutex, MutexGuard},
|
mutex::{Mutex, MutexGuard},
|
||||||
TexturesDelta,
|
TexturesDelta,
|
||||||
};
|
};
|
||||||
|
pub use egui::{pos2, Color32};
|
||||||
use crate::{epi, App};
|
|
||||||
|
|
||||||
use super::{web_painter::WebPainter, *};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -284,7 +284,7 @@ impl AppRunner {
|
||||||
/// Get mutable access to the concrete [`App`] we enclose.
|
/// Get mutable access to the concrete [`App`] we enclose.
|
||||||
///
|
///
|
||||||
/// This will panic if your app does not implement [`App::as_any_mut`].
|
/// This will panic if your app does not implement [`App::as_any_mut`].
|
||||||
pub fn app_mut<ConreteApp: 'static + App>(&mut self) -> &mut ConreteApp {
|
pub fn app_mut<ConreteApp: 'static + crate::App>(&mut self) -> &mut ConreteApp {
|
||||||
self.app
|
self.app
|
||||||
.as_any_mut()
|
.as_any_mut()
|
||||||
.expect("Your app must implement `as_any_mut`, but it doesn't")
|
.expect("Your app must implement `as_any_mut`, but it doesn't")
|
||||||
|
@ -313,11 +313,10 @@ 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(|m| m.clone());
|
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||||
self.egui_ctx
|
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||||
.memory_mut(|m| m.set_everything_is_visible(true));
|
|
||||||
self.logic()?;
|
self.logic()?;
|
||||||
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
|
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||||
self.egui_ctx.clear_animations();
|
self.egui_ctx.clear_animations();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -389,7 +388,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(|o| o.screen_reader) {
|
if self.egui_ctx.options().screen_reader {
|
||||||
self.screen_reader
|
self.screen_reader
|
||||||
.speak(&platform_output.events_description());
|
.speak(&platform_output.events_description());
|
||||||
}
|
}
|
||||||
|
@ -401,8 +400,6 @@ impl AppRunner {
|
||||||
events: _, // already handled
|
events: _, // already handled
|
||||||
mutable_text_under_cursor,
|
mutable_text_under_cursor,
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_update: _, // not currently implemented
|
|
||||||
} = platform_output;
|
} = platform_output;
|
||||||
|
|
||||||
set_cursor_icon(cursor_icon);
|
set_cursor_icon(cursor_icon);
|
||||||
|
@ -450,6 +447,8 @@ 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(
|
||||||
|
@ -466,7 +465,6 @@ impl EventToUnsubscribe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppRunnerContainer {
|
pub struct AppRunnerContainer {
|
||||||
pub runner: AppRunnerRef,
|
pub runner: AppRunnerRef,
|
||||||
|
|
||||||
|
@ -485,6 +483,8 @@ 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,8 +1,5 @@
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
|
|
||||||
use egui::Key;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
struct IsDestroyed(pub bool);
|
struct IsDestroyed(pub bool);
|
||||||
|
|
||||||
|
@ -31,6 +28,7 @@ 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())?;
|
||||||
|
@ -66,13 +64,11 @@ 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) = egui_key {
|
if let Some(key) = translate_key(&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,
|
||||||
repeat: false, // egui will fill this in for us!
|
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -88,18 +84,10 @@ 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();
|
||||||
|
|
||||||
#[allow(clippy::if_same_then_else)]
|
let prevent_default = if matches!(event.key().as_str(), "Tab") {
|
||||||
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(),
|
||||||
|
@ -123,7 +111,6 @@ 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();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
@ -138,7 +125,6 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
repeat: false,
|
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -210,21 +196,15 @@ 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):
|
||||||
"contextmenu",
|
let event_name = "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)?;
|
||||||
|
@ -408,7 +388,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 winit.
|
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
|
||||||
points_per_scroll_line
|
points_per_scroll_line
|
||||||
}
|
}
|
||||||
_ => 1.0, // DOM_DELTA_PIXEL
|
_ => 1.0, // DOM_DELTA_PIXEL
|
||||||
|
|
|
@ -83,6 +83,7 @@ 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()
|
||||||
|
@ -90,7 +91,7 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||||
|
|
||||||
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||||
canvas_element(canvas_id)
|
canvas_element(canvas_id)
|
||||||
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
|
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
||||||
|
|
|
@ -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_mut(|m| *m = memory);
|
*ctx.memory() = 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 ctx.memory(|mem| ron::to_string(mem)) {
|
match ron::to_string(&*ctx.memory()) {
|
||||||
Ok(ron) => {
|
Ok(ron) => {
|
||||||
local_storage_set("egui_memory_ron", &ron);
|
local_storage_set("egui_memory_ron", &ron);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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()
|
||||||
|
@ -22,6 +23,7 @@ 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");
|
||||||
|
@ -127,6 +129,7 @@ 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,3 +1,4 @@
|
||||||
|
use egui::Rgba;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
/// Renderer for a browser canvas.
|
/// Renderer for a browser canvas.
|
||||||
|
@ -18,7 +19,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: [f32; 4],
|
clear_color: Rgba,
|
||||||
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,6 +2,7 @@ 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};
|
||||||
|
@ -48,7 +49,7 @@ impl WebPainter for WebPainterGlow {
|
||||||
|
|
||||||
fn paint_and_update_textures(
|
fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
clear_color: [f32; 4],
|
clear_color: Rgba,
|
||||||
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;
|
use egui::{mutex::RwLock, Rgba};
|
||||||
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
||||||
|
|
||||||
use crate::WebOptions;
|
use crate::WebOptions;
|
||||||
|
@ -49,7 +49,6 @@ 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())
|
||||||
})
|
})
|
||||||
|
@ -61,13 +60,8 @@ 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(wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(options.wgpu_options.backends);
|
||||||
backends: options.wgpu_options.backends,
|
let surface = instance.create_surface_from_canvas(&canvas);
|
||||||
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 {
|
||||||
|
@ -87,7 +81,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_capabilities(&adapter).formats);
|
egui_wgpu::preferred_framebuffer_format(&surface.get_supported_formats(&adapter));
|
||||||
|
|
||||||
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);
|
||||||
|
@ -105,7 +99,6 @@ 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.");
|
||||||
|
@ -135,7 +128,7 @@ impl WebPainter for WebPainterWgpu {
|
||||||
|
|
||||||
fn paint_and_update_textures(
|
fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
clear_color: [f32; 4],
|
clear_color: Rgba,
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &egui::TexturesDelta,
|
||||||
|
@ -228,10 +221,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[0] as f64,
|
r: clear_color.r() as f64,
|
||||||
g: clear_color[1] as f64,
|
g: clear_color.g() as f64,
|
||||||
b: clear_color[2] as f64,
|
b: clear_color.b() as f64,
|
||||||
a: clear_color[3] as f64,
|
a: clear_color.a() as f64,
|
||||||
}),
|
}),
|
||||||
store: true,
|
store: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,22 +3,10 @@ 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)).
|
|
||||||
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
|
|
||||||
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
|
|
||||||
* `winit::Painter` now supports transparent backbuffer ([#2684](https://github.com/emilk/egui/pull/2684)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08 - web support
|
|
||||||
* Renamed `RenderPass` to `Renderer`.
|
* Renamed `RenderPass` to `Renderer`.
|
||||||
* Renamed `RenderPass::execute` to `RenderPass::render`.
|
* Renamed `RenderPass::execute` to `RenderPass::render`.
|
||||||
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
|
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
|
||||||
* Reexported `Renderer`.
|
* Reexported `Renderer`.
|
||||||
* You can now use `egui-wgpu` on web, using WebGL ([#2107](https://github.com/emilk/egui/pull/2107)).
|
|
||||||
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
|
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||||
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
|
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||||
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
|
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
|
||||||
|
@ -27,7 +15,6 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
|
||||||
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
|
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
|
||||||
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
|
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
|
||||||
|
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
|
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
|
||||||
* Make `RenderPass` `Send` and `Sync` ([#1883](https://github.com/emilk/egui/pull/1883)).
|
* Make `RenderPass` `Send` and `Sync` ([#1883](https://github.com/emilk/egui/pull/1883)).
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.21.0"
|
version = "0.19.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>",
|
||||||
|
@ -32,24 +32,25 @@ all-features = true
|
||||||
puffin = ["dep:puffin"]
|
puffin = ["dep:puffin"]
|
||||||
|
|
||||||
## Enable [`winit`](https://docs.rs/winit) integration.
|
## Enable [`winit`](https://docs.rs/winit) integration.
|
||||||
winit = ["dep:winit"]
|
winit = ["dep:pollster", "dep:winit"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
|
egui = { version = "0.19.0", path = "../egui", 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.15.0"
|
wgpu = "0.14"
|
||||||
|
|
||||||
#! ### 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.28", optional = true }
|
pollster = { version = "0.2", optional = true }
|
||||||
|
winit = { version = "0.27.2", optional = true }
|
||||||
|
|
||||||
# Native:
|
# Native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
|
|
@ -8,19 +8,18 @@
|
||||||
|
|
||||||
pub use wgpu;
|
pub use wgpu;
|
||||||
|
|
||||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
/// Low-level painting of [`egui`] on [`wgpu`].
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub use renderer::CallbackFn;
|
pub use renderer::CallbackFn;
|
||||||
pub use renderer::Renderer;
|
pub use renderer::Renderer;
|
||||||
|
|
||||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
/// Module for painting [`egui`] with [`wgpu`] on [`winit`].
|
||||||
#[cfg(feature = "winit")]
|
#[cfg(feature = "winit")]
|
||||||
pub mod winit;
|
pub mod winit;
|
||||||
|
|
||||||
|
use egui::mutex::RwLock;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use epaint::mutex::RwLock;
|
|
||||||
|
|
||||||
/// Access to the render state for egui.
|
/// Access to the render state for egui.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RenderState {
|
pub struct RenderState {
|
||||||
|
@ -99,39 +98,7 @@ 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"
|
||||||
|
|
|
@ -4,13 +4,14 @@ use std::num::NonZeroU64;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
|
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
|
||||||
|
|
||||||
|
use egui::epaint::Vertex;
|
||||||
|
use egui::NumExt;
|
||||||
|
use egui::{epaint::Primitive, PaintCallbackInfo};
|
||||||
use type_map::concurrent::TypeMap;
|
use type_map::concurrent::TypeMap;
|
||||||
use wgpu;
|
use wgpu;
|
||||||
use wgpu::util::DeviceExt as _;
|
use wgpu::util::DeviceExt as _;
|
||||||
|
|
||||||
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
|
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU
|
||||||
|
|
||||||
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU
|
|
||||||
/// rendering.
|
/// rendering.
|
||||||
///
|
///
|
||||||
/// The callback is composed of two functions: `prepare` and `paint`:
|
/// The callback is composed of two functions: `prepare` and `paint`:
|
||||||
|
@ -153,11 +154,11 @@ pub struct Renderer {
|
||||||
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
||||||
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
|
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
|
||||||
/// sampler.
|
/// sampler.
|
||||||
textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
|
textures: HashMap<egui::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
|
||||||
next_user_texture_id: u64,
|
next_user_texture_id: u64,
|
||||||
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
samplers: HashMap<egui::TextureOptions, wgpu::Sampler>,
|
||||||
|
|
||||||
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
|
/// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render
|
||||||
/// pipelines that must have the lifetime of the renderpass.
|
/// pipelines that must have the lifetime of the renderpass.
|
||||||
pub paint_callback_resources: TypeMap,
|
pub paint_callback_resources: TypeMap,
|
||||||
}
|
}
|
||||||
|
@ -345,7 +346,7 @@ impl Renderer {
|
||||||
pub fn render<'rp>(
|
pub fn render<'rp>(
|
||||||
&'rp self,
|
&'rp self,
|
||||||
render_pass: &mut wgpu::RenderPass<'rp>,
|
render_pass: &mut wgpu::RenderPass<'rp>,
|
||||||
paint_jobs: &[epaint::ClippedPrimitive],
|
paint_jobs: &[egui::epaint::ClippedPrimitive],
|
||||||
screen_descriptor: &ScreenDescriptor,
|
screen_descriptor: &ScreenDescriptor,
|
||||||
) {
|
) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
@ -360,7 +361,7 @@ impl Renderer {
|
||||||
let mut index_buffer_slices = self.index_buffer.slices.iter();
|
let mut index_buffer_slices = self.index_buffer.slices.iter();
|
||||||
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
|
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
|
||||||
|
|
||||||
for epaint::ClippedPrimitive {
|
for egui::ClippedPrimitive {
|
||||||
clip_rect,
|
clip_rect,
|
||||||
primitive,
|
primitive,
|
||||||
} in paint_jobs
|
} in paint_jobs
|
||||||
|
@ -474,8 +475,8 @@ impl Renderer {
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
id: epaint::TextureId,
|
id: egui::TextureId,
|
||||||
image_delta: &epaint::ImageDelta,
|
image_delta: &egui::epaint::ImageDelta,
|
||||||
) {
|
) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
|
@ -489,7 +490,7 @@ impl Renderer {
|
||||||
};
|
};
|
||||||
|
|
||||||
let data_color32 = match &image_delta.image {
|
let data_color32 = match &image_delta.image {
|
||||||
epaint::ImageData::Color(image) => {
|
egui::ImageData::Color(image) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
width as usize * height as usize,
|
width as usize * height as usize,
|
||||||
image.pixels.len(),
|
image.pixels.len(),
|
||||||
|
@ -497,7 +498,7 @@ impl Renderer {
|
||||||
);
|
);
|
||||||
Cow::Borrowed(&image.pixels)
|
Cow::Borrowed(&image.pixels)
|
||||||
}
|
}
|
||||||
epaint::ImageData::Font(image) => {
|
egui::ImageData::Font(image) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
width as usize * height as usize,
|
width as usize * height as usize,
|
||||||
image.pixels.len(),
|
image.pixels.len(),
|
||||||
|
@ -554,7 +555,6 @@ 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
|
||||||
|
@ -582,7 +582,7 @@ impl Renderer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free_texture(&mut self, id: &epaint::TextureId) {
|
pub fn free_texture(&mut self, id: &egui::TextureId) {
|
||||||
self.textures.remove(id);
|
self.textures.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,15 +590,15 @@ impl Renderer {
|
||||||
///
|
///
|
||||||
/// This could be used by custom paint hooks to render images that have been added through with
|
/// This could be used by custom paint hooks to render images that have been added through with
|
||||||
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
||||||
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
/// or [`egui::Context::load_texture`].
|
||||||
pub fn texture(
|
pub fn texture(
|
||||||
&self,
|
&self,
|
||||||
id: &epaint::TextureId,
|
id: &egui::TextureId,
|
||||||
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
|
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
|
||||||
self.textures.get(id)
|
self.textures.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
|
/// Registers a `wgpu::Texture` with a `egui::TextureId`.
|
||||||
///
|
///
|
||||||
/// This enables the application to reference the texture inside an image ui element.
|
/// This enables the application to reference the texture inside an image ui element.
|
||||||
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
||||||
|
@ -609,7 +609,7 @@ impl Renderer {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
texture: &wgpu::TextureView,
|
texture: &wgpu::TextureView,
|
||||||
texture_filter: wgpu::FilterMode,
|
texture_filter: wgpu::FilterMode,
|
||||||
) -> epaint::TextureId {
|
) -> egui::TextureId {
|
||||||
self.register_native_texture_with_sampler_options(
|
self.register_native_texture_with_sampler_options(
|
||||||
device,
|
device,
|
||||||
texture,
|
texture,
|
||||||
|
@ -622,7 +622,7 @@ impl Renderer {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
|
/// Registers a `wgpu::Texture` with an existing `egui::TextureId`.
|
||||||
///
|
///
|
||||||
/// This enables applications to reuse `TextureId`s.
|
/// This enables applications to reuse `TextureId`s.
|
||||||
pub fn update_egui_texture_from_wgpu_texture(
|
pub fn update_egui_texture_from_wgpu_texture(
|
||||||
|
@ -630,7 +630,7 @@ impl Renderer {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
texture: &wgpu::TextureView,
|
texture: &wgpu::TextureView,
|
||||||
texture_filter: wgpu::FilterMode,
|
texture_filter: wgpu::FilterMode,
|
||||||
id: epaint::TextureId,
|
id: egui::TextureId,
|
||||||
) {
|
) {
|
||||||
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
|
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||||
device,
|
device,
|
||||||
|
@ -645,7 +645,7 @@ impl Renderer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
|
/// Registers a `wgpu::Texture` with a `egui::TextureId` while also accepting custom
|
||||||
/// `wgpu::SamplerDescriptor` options.
|
/// `wgpu::SamplerDescriptor` options.
|
||||||
///
|
///
|
||||||
/// This allows applications to specify individual minification/magnification filters as well as
|
/// This allows applications to specify individual minification/magnification filters as well as
|
||||||
|
@ -660,7 +660,7 @@ impl Renderer {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
texture: &wgpu::TextureView,
|
texture: &wgpu::TextureView,
|
||||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||||
) -> epaint::TextureId {
|
) -> egui::TextureId {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
@ -683,14 +683,14 @@ impl Renderer {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = epaint::TextureId::User(self.next_user_texture_id);
|
let id = egui::TextureId::User(self.next_user_texture_id);
|
||||||
self.textures.insert(id, (None, bind_group));
|
self.textures.insert(id, (None, bind_group));
|
||||||
self.next_user_texture_id += 1;
|
self.next_user_texture_id += 1;
|
||||||
|
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
|
/// Registers a `wgpu::Texture` with an existing `egui::TextureId` while also accepting custom
|
||||||
/// `wgpu::SamplerDescriptor` options.
|
/// `wgpu::SamplerDescriptor` options.
|
||||||
///
|
///
|
||||||
/// This allows applications to reuse `TextureId`s created with custom sampler options.
|
/// This allows applications to reuse `TextureId`s created with custom sampler options.
|
||||||
|
@ -700,7 +700,7 @@ impl Renderer {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
texture: &wgpu::TextureView,
|
texture: &wgpu::TextureView,
|
||||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||||
id: epaint::TextureId,
|
id: egui::TextureId,
|
||||||
) {
|
) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
|
@ -741,7 +741,7 @@ impl Renderer {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
encoder: &mut wgpu::CommandEncoder,
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
paint_jobs: &[epaint::ClippedPrimitive],
|
paint_jobs: &[egui::epaint::ClippedPrimitive],
|
||||||
screen_descriptor: &ScreenDescriptor,
|
screen_descriptor: &ScreenDescriptor,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
@ -801,7 +801,7 @@ impl Renderer {
|
||||||
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
|
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
|
||||||
|
|
||||||
crate::profile_scope!("primitives");
|
crate::profile_scope!("primitives");
|
||||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
|
for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
|
||||||
match primitive {
|
match primitive {
|
||||||
Primitive::Mesh(mesh) => {
|
Primitive::Mesh(mesh) => {
|
||||||
{
|
{
|
||||||
|
@ -844,17 +844,14 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_sampler(
|
fn create_sampler(options: egui::TextureOptions, device: &wgpu::Device) -> wgpu::Sampler {
|
||||||
options: epaint::textures::TextureOptions,
|
|
||||||
device: &wgpu::Device,
|
|
||||||
) -> wgpu::Sampler {
|
|
||||||
let mag_filter = match options.magnification {
|
let mag_filter = match options.magnification {
|
||||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||||
};
|
};
|
||||||
let min_filter = match options.minification {
|
let min_filter = match options.minification {
|
||||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||||
};
|
};
|
||||||
device.create_sampler(&wgpu::SamplerDescriptor {
|
device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
label: Some(&format!(
|
label: Some(&format!(
|
||||||
|
@ -896,7 +893,7 @@ struct ScissorRect {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScissorRect {
|
impl ScissorRect {
|
||||||
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
fn new(clip_rect: &egui::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
||||||
// Transform clip rect to physical pixels:
|
// Transform clip rect to physical pixels:
|
||||||
let clip_min_x = pixels_per_point * clip_rect.min.x;
|
let clip_min_x = pixels_per_point * clip_rect.min.x;
|
||||||
let clip_min_y = pixels_per_point * clip_rect.min.y;
|
let clip_min_y = pixels_per_point * clip_rect.min.y;
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use epaint::mutex::RwLock;
|
use egui::mutex::RwLock;
|
||||||
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
use wgpu::{Adapter, Instance, Surface};
|
||||||
|
|
||||||
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
||||||
|
|
||||||
struct SurfaceState {
|
struct SurfaceState {
|
||||||
surface: wgpu::Surface,
|
surface: Surface,
|
||||||
alpha_mode: wgpu::CompositeAlphaMode,
|
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
}
|
}
|
||||||
|
@ -19,12 +18,11 @@ 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: wgpu::Instance,
|
instance: Instance,
|
||||||
adapter: Option<wgpu::Adapter>,
|
adapter: Option<Adapter>,
|
||||||
render_state: Option<RenderState>,
|
render_state: Option<RenderState>,
|
||||||
surface_state: Option<SurfaceState>,
|
surface_state: Option<SurfaceState>,
|
||||||
}
|
}
|
||||||
|
@ -42,21 +40,12 @@ 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(
|
pub fn new(configuration: WgpuConfiguration, msaa_samples: u32, depth_bits: u8) -> Self {
|
||||||
configuration: WgpuConfiguration,
|
let instance = wgpu::Instance::new(configuration.backends);
|
||||||
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,
|
||||||
|
|
||||||
|
@ -71,27 +60,26 @@ impl Painter {
|
||||||
///
|
///
|
||||||
/// Will return [`None`] if the render state has not been initialized yet.
|
/// Will return [`None`] if the render state has not been initialized yet.
|
||||||
pub fn render_state(&self) -> Option<RenderState> {
|
pub fn render_state(&self) -> Option<RenderState> {
|
||||||
self.render_state.clone()
|
self.render_state.as_ref().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_render_state(
|
async fn init_render_state(
|
||||||
&self,
|
&self,
|
||||||
adapter: &wgpu::Adapter,
|
adapter: &Adapter,
|
||||||
target_format: wgpu::TextureFormat,
|
target_format: wgpu::TextureFormat,
|
||||||
) -> Result<RenderState, wgpu::RequestDeviceError> {
|
) -> RenderState {
|
||||||
adapter
|
let (device, queue) =
|
||||||
.request_device(&self.configuration.device_descriptor, None)
|
pollster::block_on(adapter.request_device(&self.configuration.device_descriptor, None))
|
||||||
.await
|
.unwrap();
|
||||||
.map(|(device, queue)| {
|
|
||||||
let renderer =
|
let renderer = Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
|
||||||
Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
|
|
||||||
RenderState {
|
RenderState {
|
||||||
device: Arc::new(device),
|
device: Arc::new(device),
|
||||||
queue: Arc::new(queue),
|
queue: Arc::new(queue),
|
||||||
target_format,
|
target_format,
|
||||||
renderer: Arc::new(RwLock::new(renderer)),
|
renderer: Arc::new(RwLock::new(renderer)),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to defer the initialization of our render state until we have a surface
|
// We want to defer the initialization of our render state until we have a surface
|
||||||
|
@ -99,52 +87,54 @@ impl Painter {
|
||||||
//
|
//
|
||||||
// After we've initialized our render state once though we expect all future surfaces
|
// After we've initialized our render state once though we expect all future surfaces
|
||||||
// will have the same format and so this render state will remain valid.
|
// will have the same format and so this render state will remain valid.
|
||||||
async fn ensure_render_state_for_surface(
|
fn ensure_render_state_for_surface(&mut self, surface: &Surface) {
|
||||||
&mut self,
|
self.adapter.get_or_insert_with(|| {
|
||||||
surface: &wgpu::Surface,
|
pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
) -> Result<(), wgpu::RequestDeviceError> {
|
|
||||||
if self.adapter.is_none() {
|
|
||||||
self.adapter = self
|
|
||||||
.instance
|
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
|
||||||
power_preference: self.configuration.power_preference,
|
power_preference: self.configuration.power_preference,
|
||||||
compatible_surface: Some(surface),
|
compatible_surface: Some(surface),
|
||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
})
|
}))
|
||||||
.await;
|
.unwrap()
|
||||||
}
|
});
|
||||||
|
|
||||||
if self.render_state.is_none() {
|
if self.render_state.is_none() {
|
||||||
match &self.adapter {
|
let adapter = self.adapter.as_ref().unwrap();
|
||||||
Some(adapter) => {
|
|
||||||
let swapchain_format = crate::preferred_framebuffer_format(
|
let swapchain_format =
|
||||||
&surface.get_capabilities(adapter).formats,
|
crate::preferred_framebuffer_format(&surface.get_supported_formats(adapter));
|
||||||
);
|
|
||||||
let rs = self.init_render_state(adapter, swapchain_format).await?;
|
let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format));
|
||||||
self.render_state = Some(rs);
|
self.render_state = Some(rs);
|
||||||
}
|
}
|
||||||
None => return Err(wgpu::RequestDeviceError {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_surface(
|
fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
|
||||||
surface_state: &SurfaceState,
|
crate::profile_function!();
|
||||||
render_state: &RenderState,
|
|
||||||
present_mode: wgpu::PresentMode,
|
let render_state = self
|
||||||
) {
|
.render_state
|
||||||
surface_state.surface.configure(
|
.as_ref()
|
||||||
&render_state.device,
|
.expect("Render state should exist before surface configuration");
|
||||||
&wgpu::SurfaceConfiguration {
|
let format = render_state.target_format;
|
||||||
|
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
format: render_state.target_format,
|
format,
|
||||||
width: surface_state.width,
|
width: width_in_pixels,
|
||||||
height: surface_state.height,
|
height: height_in_pixels,
|
||||||
present_mode,
|
present_mode: self.configuration.present_mode,
|
||||||
alpha_mode: surface_state.alpha_mode,
|
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||||
view_formats: vec![render_state.target_format],
|
};
|
||||||
},
|
|
||||||
);
|
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`]
|
||||||
|
@ -171,53 +161,27 @@ impl Painter {
|
||||||
/// The raw Window handle associated with the given `window` must be a valid object to create a
|
/// The raw Window handle associated with the given `window` must be a valid object to create a
|
||||||
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
|
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
|
||||||
/// be cleared by passing `None`).
|
/// be cleared by passing `None`).
|
||||||
///
|
pub unsafe fn set_window(&mut self, window: Option<&winit::window::Window>) {
|
||||||
/// # Errors
|
|
||||||
/// If the provided wgpu configuration does not match an available device.
|
|
||||||
pub async unsafe fn set_window(
|
|
||||||
&mut self,
|
|
||||||
window: Option<&winit::window::Window>,
|
|
||||||
) -> Result<(), crate::WgpuError> {
|
|
||||||
match window {
|
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);
|
||||||
|
|
||||||
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: size.width,
|
width,
|
||||||
height: size.height,
|
height,
|
||||||
alpha_mode,
|
|
||||||
});
|
});
|
||||||
self.resize_and_generate_depth_texture_view(size.width, size.height);
|
self.resize_and_generate_depth_texture_view(width, height);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.surface_state = None;
|
self.surface_state = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the maximum texture dimension supported if known
|
/// Returns the maximum texture dimension supported if known
|
||||||
|
@ -231,22 +195,15 @@ impl Painter {
|
||||||
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_and_generate_depth_texture_view(
|
pub fn resize_and_generate_depth_texture_view(
|
||||||
&mut self,
|
&mut self,
|
||||||
width_in_pixels: u32,
|
width_in_pixels: u32,
|
||||||
height_in_pixels: u32,
|
height_in_pixels: u32,
|
||||||
) {
|
) {
|
||||||
let render_state = self.render_state.as_ref().unwrap();
|
self.configure_surface(width_in_pixels, height_in_pixels);
|
||||||
let surface_state = self.surface_state.as_mut().unwrap();
|
let device = &self.render_state.as_ref().unwrap().device;
|
||||||
|
|
||||||
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| {
|
||||||
render_state
|
device
|
||||||
.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 {
|
||||||
|
@ -260,7 +217,6 @@ 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())
|
||||||
});
|
});
|
||||||
|
@ -277,9 +233,9 @@ 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: [f32; 4],
|
clear_color: egui::Rgba,
|
||||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
textures_delta: &epaint::textures::TexturesDelta,
|
textures_delta: &egui::TexturesDelta,
|
||||||
) {
|
) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
|
@ -291,6 +247,7 @@ 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");
|
||||||
|
@ -303,11 +260,7 @@ 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(
|
self.configure_surface(width, height);
|
||||||
surface_state,
|
|
||||||
render_state,
|
|
||||||
self.configuration.present_mode,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SurfaceErrorAction::SkipFrame => {
|
SurfaceErrorAction::SkipFrame => {
|
||||||
|
@ -325,7 +278,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: [surface_state.width, surface_state.height],
|
size_in_pixels: [width, height],
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -360,10 +313,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[0] as f64,
|
r: clear_color.r() as f64,
|
||||||
g: clear_color[1] as f64,
|
g: clear_color.g() as f64,
|
||||||
b: clear_color[2] as f64,
|
b: clear_color.b() as f64,
|
||||||
a: clear_color[3] as f64,
|
a: clear_color.a() as f64,
|
||||||
}),
|
}),
|
||||||
store: true,
|
store: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,28 +3,8 @@ All notable changes to the `egui-winit` integration will be noted in this file.
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||||
|
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||||
## 0.21.1 - 2023-02-12
|
|
||||||
* Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.21.0 - 2023-02-08
|
|
||||||
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
|
|
||||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
|
||||||
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
|
||||||
* Fix bug where the cursor could get stuck using the wrong icon.
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.1 - 2022-12-11
|
|
||||||
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08
|
|
||||||
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)).
|
|
||||||
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).
|
|
||||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
|
||||||
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.21.1"
|
version = "0.19.0"
|
||||||
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"
|
||||||
|
@ -20,9 +20,6 @@ all-features = true
|
||||||
[features]
|
[features]
|
||||||
default = ["clipboard", "links", "wayland", "winit/default"]
|
default = ["clipboard", "links", "wayland", "winit/default"]
|
||||||
|
|
||||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
|
||||||
accesskit = ["accesskit_winit", "egui/accesskit"]
|
|
||||||
|
|
||||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
||||||
bytemuck = ["egui/bytemuck"]
|
bytemuck = ["egui/bytemuck"]
|
||||||
|
|
||||||
|
@ -36,6 +33,9 @@ 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"]
|
||||||
|
|
||||||
|
@ -43,34 +43,30 @@ serde = ["egui/serde", "dep:serde"]
|
||||||
wayland = ["winit/wayland"]
|
wayland = ["winit/wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
egui = { version = "0.19.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.28", default-features = false }
|
winit = { version = "0.27.2", 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 }
|
||||||
|
|
||||||
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"] }
|
||||||
|
|
||||||
webbrowser = { version = "0.8.3", optional = true }
|
# feature screen_reader
|
||||||
|
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"] }
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ impl Clipboard {
|
||||||
Self {
|
Self {
|
||||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||||
arboard: init_arboard(),
|
arboard: init_arboard(),
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
any(
|
any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
|
@ -42,7 +41,6 @@ impl Clipboard {
|
||||||
feature = "smithay-clipboard"
|
feature = "smithay-clipboard"
|
||||||
))]
|
))]
|
||||||
smithay: init_smithay_clipboard(wayland_display),
|
smithay: init_smithay_clipboard(wayland_display),
|
||||||
|
|
||||||
clipboard: Default::default(),
|
clipboard: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +60,7 @@ impl Clipboard {
|
||||||
return match clipboard.load() {
|
return match clipboard.load() {
|
||||||
Ok(text) => Some(text),
|
Ok(text) => Some(text),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("smithay paste error: {err}");
|
tracing::error!("Paste error: {}", err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -73,7 +71,7 @@ impl Clipboard {
|
||||||
return match clipboard.get_text() {
|
return match clipboard.get_text() {
|
||||||
Ok(text) => Some(text),
|
Ok(text) => Some(text),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("arboard paste error: {err}");
|
tracing::error!("Paste error: {}", err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -101,7 +99,7 @@ impl Clipboard {
|
||||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||||
if let Some(clipboard) = &mut self.arboard {
|
if let Some(clipboard) = &mut self.arboard {
|
||||||
if let Err(err) = clipboard.set_text(text) {
|
if let Err(err) = clipboard.set_text(text) {
|
||||||
tracing::error!("arboard copy/cut error: {err}");
|
tracing::error!("Copy/Cut error: {}", err);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -112,11 +110,10 @@ impl Clipboard {
|
||||||
|
|
||||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||||
fn init_arboard() -> Option<arboard::Clipboard> {
|
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||||
tracing::debug!("Initializing arboard clipboard…");
|
|
||||||
match arboard::Clipboard::new() {
|
match arboard::Clipboard::new() {
|
||||||
Ok(clipboard) => Some(clipboard),
|
Ok(clipboard) => Some(clipboard),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("Failed to initialize arboard clipboard: {err}");
|
tracing::error!("Failed to initialize clipboard: {}", err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,11 +133,10 @@ fn init_smithay_clipboard(
|
||||||
wayland_display: Option<*mut c_void>,
|
wayland_display: Option<*mut c_void>,
|
||||||
) -> Option<smithay_clipboard::Clipboard> {
|
) -> Option<smithay_clipboard::Clipboard> {
|
||||||
if let Some(display) = wayland_display {
|
if let Some(display) = wayland_display {
|
||||||
tracing::debug!("Initializing smithay clipboard…");
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
|
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
|
tracing::error!("Cannot initialize smithay clipboard without a display handle!");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,20 +11,27 @@
|
||||||
|
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub use accesskit_winit;
|
|
||||||
pub use egui;
|
pub use egui;
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -59,12 +66,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: Option<egui::CursorIcon>,
|
current_cursor_icon: 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.
|
||||||
|
@ -79,9 +86,6 @@ pub struct State {
|
||||||
|
|
||||||
/// track ime state
|
/// track ime state
|
||||||
input_method_editor_started: bool,
|
input_method_editor_started: bool,
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit: Option<accesskit_winit::Adapter>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -100,35 +104,19 @@ 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: None,
|
current_cursor_icon: egui::CursorIcon::Default,
|
||||||
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,
|
||||||
|
|
||||||
input_method_editor_started: false,
|
input_method_editor_started: false,
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
|
|
||||||
&mut self,
|
|
||||||
window: &winit::window::Window,
|
|
||||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
|
||||||
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
|
|
||||||
) {
|
|
||||||
self.accesskit = Some(accesskit_winit::Adapter::new(
|
|
||||||
window,
|
|
||||||
initial_tree_update_factory,
|
|
||||||
event_loop_proxy,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
||||||
/// that egui will use.
|
/// that egui will use.
|
||||||
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
||||||
|
@ -368,9 +356,8 @@ impl State {
|
||||||
consumed: false,
|
consumed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WindowEvent::AxisMotion { .. }
|
||||||
// Things that may require repaint:
|
| WindowEvent::CloseRequested
|
||||||
WindowEvent::CloseRequested
|
|
||||||
| WindowEvent::CursorEntered { .. }
|
| WindowEvent::CursorEntered { .. }
|
||||||
| WindowEvent::Destroyed
|
| WindowEvent::Destroyed
|
||||||
| WindowEvent::Occluded(_)
|
| WindowEvent::Occluded(_)
|
||||||
|
@ -380,38 +367,12 @@ impl State {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
},
|
},
|
||||||
|
WindowEvent::Moved(_) => EventResponse {
|
||||||
// Things we completely ignore:
|
repaint: false, // moving a window doesn't warrant a repaint
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call this when there is a new [`accesskit::ActionRequest`].
|
|
||||||
///
|
|
||||||
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
|
||||||
self.egui_input
|
|
||||||
.events
|
|
||||||
.push(egui::Event::AccessKitActionRequest(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_mouse_button_input(
|
fn on_mouse_button_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -599,7 +560,6 @@ impl State {
|
||||||
self.egui_input.events.push(egui::Event::Key {
|
self.egui_input.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed,
|
pressed,
|
||||||
repeat: false, // egui will fill this in for us!
|
|
||||||
modifiers: self.egui_input.modifiers,
|
modifiers: self.egui_input.modifiers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -620,6 +580,11 @@ 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,
|
||||||
|
@ -627,8 +592,6 @@ impl State {
|
||||||
events: _, // handled above
|
events: _, // handled above
|
||||||
mutable_text_under_cursor: _, // only used in eframe web
|
mutable_text_under_cursor: _, // only used in eframe web
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_update,
|
|
||||||
} = platform_output;
|
} = platform_output;
|
||||||
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
||||||
|
|
||||||
|
@ -645,35 +608,25 @@ impl State {
|
||||||
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
||||||
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
if let Some(accesskit) = self.accesskit.as_ref() {
|
|
||||||
if let Some(update) = accesskit_update {
|
|
||||||
accesskit.update_if_active(|| update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
||||||
if self.current_cursor_icon == Some(cursor_icon) {
|
// prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing
|
||||||
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
#[cfg(windows)]
|
||||||
// On other platforms: just early-out to save CPU.
|
if self.current_cursor_icon == cursor_icon {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.current_cursor_icon = cursor_icon;
|
||||||
|
|
||||||
|
if let Some(cursor_icon) = translate_cursor(cursor_icon) {
|
||||||
|
window.set_cursor_visible(true);
|
||||||
|
|
||||||
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
||||||
if is_pointer_in_window {
|
if is_pointer_in_window {
|
||||||
self.current_cursor_icon = Some(cursor_icon);
|
window.set_cursor_icon(cursor_icon);
|
||||||
|
|
||||||
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
|
||||||
window.set_cursor_visible(true);
|
|
||||||
window.set_cursor_icon(winit_cursor_icon);
|
|
||||||
} else {
|
|
||||||
window.set_cursor_visible(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remember to set the cursor again once the cursor returns to the screen:
|
window.set_cursor_visible(false);
|
||||||
self.current_cursor_icon = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -882,7 +835,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
49
crates/egui-winit/src/screen_reader.rs
Normal file
49
crates/egui-winit/src/screen_reader.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
pub struct ScreenReader {
|
||||||
|
#[cfg(feature = "tts")]
|
||||||
|
tts: Option<tts::Tts>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tts"))]
|
||||||
|
#[allow(clippy::derivable_impls)] // False positive
|
||||||
|
impl Default for ScreenReader {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tts")]
|
||||||
|
impl Default for ScreenReader {
|
||||||
|
fn default() -> Self {
|
||||||
|
let tts = match tts::Tts::default() {
|
||||||
|
Ok(screen_reader) => {
|
||||||
|
tracing::debug!("Initialized screen reader.");
|
||||||
|
Some(screen_reader)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Failed to load screen reader: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Self { tts }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenReader {
|
||||||
|
#[cfg(not(feature = "tts"))]
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
|
pub fn speak(&mut self, _text: &str) {}
|
||||||
|
|
||||||
|
#[cfg(feature = "tts")]
|
||||||
|
pub fn speak(&mut self, text: &str) {
|
||||||
|
if text.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(tts) = &mut self.tts {
|
||||||
|
tracing::debug!("Speaking: {:?}", text);
|
||||||
|
let interrupt = true;
|
||||||
|
if let Err(err) = tts.speak(text, interrupt) {
|
||||||
|
tracing::warn!("Failed to read: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,25 +42,23 @@ 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,
|
||||||
) -> winit::window::WindowBuilder {
|
) -> winit::window::WindowBuilder {
|
||||||
|
if !cfg!(target_os = "windows") {
|
||||||
// If the app last ran on two monitors and only one is now connected, then
|
// 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 clamping behavior is managed by the function
|
// If this happens on Windows, the window is hidden and very difficult to find.
|
||||||
// clamp_window_to_sane_position.
|
// So we don't restore window positions on Windows.
|
||||||
if let Some(pos) = self.position {
|
if let Some(pos) = self.position {
|
||||||
window = window.with_position(winit::dpi::PhysicalPosition {
|
window = window.with_position(winit::dpi::PhysicalPosition {
|
||||||
x: pos.x as f64,
|
x: pos.x as f64,
|
||||||
y: pos.y 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 {
|
||||||
window
|
window
|
||||||
|
@ -76,69 +74,4 @@ impl WindowSettings {
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
|
|
||||||
use egui::NumExt as _;
|
|
||||||
|
|
||||||
if let Some(size) = &mut self.inner_size_points {
|
|
||||||
// Prevent ridiculously small windows
|
|
||||||
let min_size = egui::Vec2::splat(64.0);
|
|
||||||
*size = size.at_least(min_size);
|
|
||||||
*size = size.at_most(max_size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clamp_window_to_sane_position<E>(
|
|
||||||
&mut self,
|
|
||||||
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
|
|
||||||
) {
|
|
||||||
if let (Some(position), Some(inner_size_points)) =
|
|
||||||
(&mut self.position, &self.inner_size_points)
|
|
||||||
{
|
|
||||||
let monitors = event_loop.available_monitors();
|
|
||||||
// default to primary monitor, in case the correct monitor was disconnected.
|
|
||||||
let mut active_monitor = if let Some(active_monitor) = event_loop
|
|
||||||
.primary_monitor()
|
|
||||||
.or_else(|| event_loop.available_monitors().next())
|
|
||||||
{
|
|
||||||
active_monitor
|
|
||||||
} else {
|
|
||||||
return; // no monitors 🤷
|
|
||||||
};
|
|
||||||
for monitor in monitors {
|
|
||||||
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
|
|
||||||
..(monitor.position().x + monitor.size().width as i32);
|
|
||||||
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
|
|
||||||
..(monitor.position().y + monitor.size().height as i32);
|
|
||||||
|
|
||||||
if monitor_x_range.contains(&(position.x as i32))
|
|
||||||
&& monitor_y_range.contains(&(position.y as i32))
|
|
||||||
{
|
|
||||||
active_monitor = monitor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
|
|
||||||
// Add size of title bar. This is 32 px by default in Win 10/11.
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
inner_size_pixels +=
|
|
||||||
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
|
|
||||||
}
|
|
||||||
let monitor_position = egui::Pos2::new(
|
|
||||||
active_monitor.position().x as f32,
|
|
||||||
active_monitor.position().y as f32,
|
|
||||||
);
|
|
||||||
let monitor_size = egui::Vec2::new(
|
|
||||||
active_monitor.size().width as f32,
|
|
||||||
active_monitor.size().height as f32,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Window size cannot be negative or the subsequent `clamp` will panic.
|
|
||||||
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
|
|
||||||
// To get the maximum position, we get the rightmost corner of the display, then
|
|
||||||
// subtract the size of the window to get the bottom right most value window.position
|
|
||||||
// can have.
|
|
||||||
*position = position.clamp(monitor_position, monitor_position + window_size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.21.0"
|
version = "0.19.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"
|
||||||
|
@ -52,14 +52,10 @@ mint = ["epaint/mint"]
|
||||||
persistence = ["serde", "epaint/serde", "ron"]
|
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"]
|
||||||
|
|
||||||
## Change Vertex layout to be compatible with unity
|
|
||||||
unity = ["epaint/unity"]
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
|
epaint = { version = "0.19.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
|
||||||
|
@ -68,10 +64,6 @@ ahash = { version = "0.8.1", default-features = false, features = [
|
||||||
nohash-hasher = "0.2"
|
nohash-hasher = "0.2"
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
## Exposes detailed accessibility implementation required by platform
|
|
||||||
## accessibility APIs. Also requires support in the egui integration.
|
|
||||||
accesskit = { version = "0.9.0", optional = true }
|
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
@ -17,9 +16,7 @@ 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,14 +5,12 @@
|
||||||
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 of the pivot
|
/// Last known pos
|
||||||
pub pivot_pos: Pos2,
|
pub 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,
|
||||||
|
@ -23,22 +21,8 @@ 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.left_top_pos(), self.size)
|
Rect::from_min_size(self.pos, self.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,12 +192,11 @@ pub(crate) struct Prepared {
|
||||||
move_response: Response,
|
move_response: Response,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
drag_bounds: Option<Rect>,
|
drag_bounds: Option<Rect>,
|
||||||
|
/// Set the first frame of new windows with anchors.
|
||||||
/// We always make windows invisible the first frame to hide "first-frame-jitters".
|
|
||||||
///
|
///
|
||||||
/// This is so that we use the first frame to calculate the window size,
|
/// This is so that we use the first frame to calculate the window size,
|
||||||
/// and then can correctly position the window and its contents the next frame,
|
/// and then can correctly position the window the next frame,
|
||||||
/// without having one frame where the window is wrongly positioned or sized.
|
/// without having one frame where the window is positioned in the wrong place.
|
||||||
temporarily_invisible: bool,
|
temporarily_invisible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,25 +230,36 @@ impl Area {
|
||||||
|
|
||||||
let layer_id = LayerId::new(order, id);
|
let layer_id = LayerId::new(order, id);
|
||||||
|
|
||||||
let state = ctx.memory(|mem| mem.areas.get(id).copied());
|
let state = ctx.memory().areas.get(id).cloned();
|
||||||
let is_new = state.is_none();
|
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 {
|
||||||
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
||||||
pivot,
|
|
||||||
size: Vec2::ZERO,
|
size: Vec2::ZERO,
|
||||||
interactable,
|
interactable,
|
||||||
});
|
});
|
||||||
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
|
state.pos = new_pos.unwrap_or(state.pos);
|
||||||
state.interactable = interactable;
|
state.interactable = interactable;
|
||||||
|
let mut temporarily_invisible = false;
|
||||||
|
|
||||||
|
if pivot != Align2::LEFT_TOP {
|
||||||
|
if is_new {
|
||||||
|
temporarily_invisible = true; // figure out the size first
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
if is_new {
|
||||||
|
temporarily_invisible = true; // figure out the size first
|
||||||
|
} else {
|
||||||
let screen = ctx.available_rect();
|
let screen = ctx.available_rect();
|
||||||
state.set_left_top_pos(
|
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||||
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
|
||||||
|
@ -292,33 +286,31 @@ 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.pivot_pos += ctx.input(|i| i.pointer.delta());
|
state.pos += ctx.input().pointer.delta();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.set_left_top_pos(
|
state.pos = ctx
|
||||||
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
.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(|m| m.areas.visible_last_frame(&layer_id))
|
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|
||||||
{
|
{
|
||||||
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
|
ctx.memory().areas.move_to_top(layer_id);
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
move_response
|
move_response
|
||||||
};
|
};
|
||||||
|
|
||||||
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
|
state.pos = ctx.round_pos_to_pixels(state.pos);
|
||||||
|
|
||||||
if constrain {
|
if constrain {
|
||||||
state.set_left_top_pos(
|
state.pos = ctx
|
||||||
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||||
.left_top(),
|
.min;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
|
@ -327,7 +319,7 @@ impl Area {
|
||||||
move_response,
|
move_response,
|
||||||
enabled,
|
enabled,
|
||||||
drag_bounds,
|
drag_bounds,
|
||||||
temporarily_invisible: is_new,
|
temporarily_invisible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +337,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(|mem| mem.areas.get(self.id).map(|area| area.rect()));
|
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
|
||||||
if let Some(area_rect) = area_rect {
|
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);
|
||||||
|
@ -374,7 +366,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.screen_rect();
|
let screen_rect = ctx.input().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
|
||||||
|
@ -390,16 +382,14 @@ impl Prepared {
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_rect = Rect::from_min_max(
|
let max_rect = Rect::from_min_max(
|
||||||
self.state.left_top_pos(),
|
self.state.pos,
|
||||||
bounds
|
bounds.max.at_least(self.state.pos + Vec2::splat(32.0)),
|
||||||
.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.left_top_pos(), bounds.max)
|
let clip_rect = Rect::from_min_max(self.state.pos, bounds.max)
|
||||||
.expand(clip_rect_margin)
|
.expand(clip_rect_margin)
|
||||||
.intersect(bounds);
|
.intersect(bounds);
|
||||||
|
|
||||||
|
@ -428,7 +418,7 @@ impl Prepared {
|
||||||
|
|
||||||
state.size = content_ui.min_rect().size();
|
state.size = content_ui.min_rect().size();
|
||||||
|
|
||||||
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
|
ctx.memory().areas.set_state(layer_id, state);
|
||||||
|
|
||||||
move_response
|
move_response
|
||||||
}
|
}
|
||||||
|
@ -436,7 +426,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(|i| i.pointer.any_pressed());
|
let any_pressed = ctx.input().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
|
||||||
|
@ -444,13 +434,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.memory(|mem| {
|
let mut existing: Vec<Rect> = ctx
|
||||||
mem.areas
|
.memory()
|
||||||
|
.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,14 +26,13 @@ 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_mut(|d| {
|
ctx.data()
|
||||||
d.get_persisted::<InnerState>(id)
|
.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_mut(|d| d.insert_persisted(self.id, self.state));
|
ctx.data().insert_persisted(self.id, self.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> Id {
|
pub fn id(&self) -> Id {
|
||||||
|
@ -65,7 +64,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(|mem| mem.everything_is_visible()) {
|
if ctx.memory().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)
|
||||||
|
@ -112,7 +111,10 @@ 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.clone().with_new_rect(icon_rect);
|
let small_icon_response = Response {
|
||||||
|
rect: icon_rect,
|
||||||
|
..response.clone()
|
||||||
|
};
|
||||||
icon_fn(ui, openness, &small_icon_response);
|
icon_fn(ui, openness, &small_icon_response);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
@ -141,10 +143,9 @@ 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 = prev_item_spacing;
|
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
|
||||||
(collapser, add_header(ui))
|
(collapser, add_header(ui))
|
||||||
});
|
});
|
||||||
HeaderResponse {
|
HeaderResponse {
|
||||||
|
@ -310,6 +311,7 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
||||||
/// Paint the arrow icon that indicated if the region is open or not
|
/// Paint the arrow icon that indicated if the region is open or not
|
||||||
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||||
let visuals = ui.style().interact(response);
|
let visuals = ui.style().interact(response);
|
||||||
|
let stroke = visuals.fg_stroke;
|
||||||
|
|
||||||
let rect = response.rect;
|
let rect = response.rect;
|
||||||
|
|
||||||
|
@ -323,11 +325,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||||
*p = rect.center() + rotation * (*p - rect.center());
|
*p = rect.center() + rotation * (*p - rect.center());
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.painter().add(Shape::convex_polygon(
|
ui.painter().add(Shape::closed_line(points, stroke));
|
||||||
points,
|
|
||||||
visuals.fg_stroke.color,
|
|
||||||
Stroke::NONE,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A function that paints an icon indicating if the region is open or not
|
/// A function that paints an icon indicating if the region is open or not
|
||||||
|
@ -554,7 +552,7 @@ impl CollapsingHeader {
|
||||||
ui.painter().add(epaint::RectShape {
|
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.weak_bg_fill,
|
fill: visuals.bg_fill,
|
||||||
stroke: visuals.bg_stroke,
|
stroke: visuals.bg_stroke,
|
||||||
// stroke: Default::default(),
|
// stroke: Default::default(),
|
||||||
});
|
});
|
||||||
|
@ -574,7 +572,10 @@ 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 = header_response.clone().with_new_rect(icon_rect);
|
let icon_response = Response {
|
||||||
|
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 {
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl ComboBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the outer width of the button and menu.
|
/// Set the width of the button and menu
|
||||||
pub fn width(mut self, width: f32) -> Self {
|
pub fn width(mut self, width: f32) -> Self {
|
||||||
self.width = Some(width);
|
self.width = Some(width);
|
||||||
self
|
self
|
||||||
|
@ -162,6 +162,9 @@ 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,
|
||||||
|
@ -169,7 +172,6 @@ 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
|
||||||
|
@ -238,58 +240,41 @@ 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(|m| m.is_popup_open(popup_id));
|
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
||||||
|
|
||||||
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
|
let popup_height = ui
|
||||||
|
.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().screen_rect().bottom()
|
< ui.ctx().input().screen_rect().bottom()
|
||||||
{
|
{
|
||||||
AboveOrBelow::Below
|
AboveOrBelow::Below
|
||||||
} else {
|
} else {
|
||||||
AboveOrBelow::Above
|
AboveOrBelow::Above
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = wrap_enabled
|
||||||
// Currently selected value's text will be wrapped if needed, so occupy the available width.
|
.then(|| ui.available_width() - ui.spacing().item_spacing.x * 2.0)
|
||||||
ui.available_width()
|
.unwrap_or_else(|| ui.spacing().slider_width);
|
||||||
} else {
|
|
||||||
// Occupy at least the minimum width assigned to ComboBox.
|
|
||||||
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
|
|
||||||
width - 2.0 * margin.x
|
|
||||||
};
|
|
||||||
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
||||||
let wrap_width = if wrap_enabled {
|
let wrap_width = wrap_enabled
|
||||||
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
|
.then(|| ui.available_width() - ui.spacing().item_spacing.x - icon_size.x)
|
||||||
ui.available_width() - icon_spacing - icon_size.x
|
.unwrap_or(f32::INFINITY);
|
||||||
} else {
|
|
||||||
// Use all the width necessary to display the currently selected value's text.
|
|
||||||
f32::INFINITY
|
|
||||||
};
|
|
||||||
|
|
||||||
let galley =
|
let galley =
|
||||||
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
|
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
|
||||||
|
|
||||||
// The width necessary to contain the whole widget with the currently selected value's text.
|
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
|
||||||
let width = if wrap_enabled {
|
|
||||||
full_minimum_width
|
|
||||||
} else {
|
|
||||||
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
|
|
||||||
galley.size().x + icon_spacing + icon_size.x
|
|
||||||
};
|
|
||||||
|
|
||||||
// Case : wrap_enabled : occupy all the available width.
|
|
||||||
// Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox,
|
|
||||||
// increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)).
|
|
||||||
let width = width.at_least(full_minimum_width);
|
let width = width.at_least(full_minimum_width);
|
||||||
let height = galley.size().y.max(icon_size.y);
|
let height = galley.size().y.max(icon_size.y);
|
||||||
|
|
||||||
|
@ -329,7 +314,7 @@ fn combo_box_dyn<'c, R>(
|
||||||
});
|
});
|
||||||
|
|
||||||
if button_response.clicked() {
|
if button_response.clicked() {
|
||||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
ui.memory().toggle_popup(popup_id);
|
||||||
}
|
}
|
||||||
let inner = crate::popup::popup_above_or_below_widget(
|
let inner = crate::popup::popup_above_or_below_widget(
|
||||||
ui,
|
ui,
|
||||||
|
@ -386,7 +371,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.weak_bg_fill,
|
fill: visuals.bg_fill,
|
||||||
stroke: visuals.bg_stroke,
|
stroke: visuals.bg_stroke,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -411,18 +396,16 @@ fn paint_default_icon(
|
||||||
match above_or_below {
|
match above_or_below {
|
||||||
AboveOrBelow::Above => {
|
AboveOrBelow::Above => {
|
||||||
// Upward pointing triangle
|
// Upward pointing triangle
|
||||||
painter.add(Shape::convex_polygon(
|
painter.add(Shape::closed_line(
|
||||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||||
visuals.fg_stroke.color,
|
visuals.fg_stroke,
|
||||||
Stroke::NONE,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
AboveOrBelow::Below => {
|
AboveOrBelow::Below => {
|
||||||
// Downward pointing triangle
|
// Downward pointing triangle
|
||||||
painter.add(Shape::convex_polygon(
|
painter.add(Shape::closed_line(
|
||||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||||
visuals.fg_stroke.color,
|
visuals.fg_stroke,
|
||||||
Stroke::NONE,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,16 +19,11 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,18 +42,18 @@ impl Frame {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn side_top_panel(style: &Style) -> Self {
|
pub(crate) fn side_top_panel(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: Margin::symmetric(8.0, 2.0),
|
inner_margin: Margin::symmetric(8.0, 2.0),
|
||||||
fill: style.visuals.panel_fill,
|
fill: style.visuals.window_fill(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn central_panel(style: &Style) -> Self {
|
pub(crate) fn central_panel(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: Margin::same(8.0),
|
inner_margin: Margin::same(8.0),
|
||||||
fill: style.visuals.panel_fill,
|
fill: style.visuals.window_fill(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +72,7 @@ impl Frame {
|
||||||
pub fn menu(style: &Style) -> Self {
|
pub fn menu(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: style.spacing.menu_margin,
|
inner_margin: style.spacing.menu_margin,
|
||||||
rounding: style.visuals.menu_rounding,
|
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||||
shadow: style.visuals.popup_shadow,
|
shadow: style.visuals.popup_shadow,
|
||||||
fill: style.visuals.window_fill(),
|
fill: style.visuals.window_fill(),
|
||||||
stroke: style.visuals.window_stroke(),
|
stroke: style.visuals.window_stroke(),
|
||||||
|
@ -87,8 +82,8 @@ impl Frame {
|
||||||
|
|
||||||
pub fn popup(style: &Style) -> Self {
|
pub fn popup(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: style.spacing.menu_margin,
|
inner_margin: style.spacing.window_margin,
|
||||||
rounding: style.visuals.menu_rounding,
|
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||||
shadow: style.visuals.popup_shadow,
|
shadow: style.visuals.popup_shadow,
|
||||||
fill: style.visuals.window_fill(),
|
fill: style.visuals.window_fill(),
|
||||||
stroke: style.visuals.window_stroke(),
|
stroke: style.visuals.window_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_mut(|d| d.get_persisted(bar_id))
|
ctx.data().get_persisted(bar_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The size of the panel (from previous frame).
|
/// The size of the panel (from previous frame).
|
||||||
|
@ -37,14 +37,14 @@ impl PanelState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ctx: &Context, bar_id: Id) {
|
fn store(self, ctx: &Context, bar_id: Id) {
|
||||||
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
|
ctx.data().insert_persisted(bar_id, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// [`Left`](Side::Left) or [`Right`](Side::Right)
|
/// [`Left`](Side::Left) or [`Right`](Side::Right)
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Side {
|
pub enum Side {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
|
@ -97,7 +97,6 @@ pub struct SidePanel {
|
||||||
id: Id,
|
id: Id,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
resizable: bool,
|
resizable: bool,
|
||||||
show_separator_line: bool,
|
|
||||||
default_width: f32,
|
default_width: f32,
|
||||||
width_range: RangeInclusive<f32>,
|
width_range: RangeInclusive<f32>,
|
||||||
}
|
}
|
||||||
|
@ -120,7 +119,6 @@ impl SidePanel {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
frame: None,
|
frame: None,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
show_separator_line: true,
|
|
||||||
default_width: 200.0,
|
default_width: 200.0,
|
||||||
width_range: 96.0..=f32::INFINITY,
|
width_range: 96.0..=f32::INFINITY,
|
||||||
}
|
}
|
||||||
|
@ -142,14 +140,6 @@ impl SidePanel {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a separator line, even when not interacting with it?
|
|
||||||
///
|
|
||||||
/// Default: `true`.
|
|
||||||
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
|
||||||
self.show_separator_line = show_separator_line;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The initial wrapping width of the [`SidePanel`].
|
/// The initial wrapping width of the [`SidePanel`].
|
||||||
pub fn default_width(mut self, default_width: f32) -> Self {
|
pub fn default_width(mut self, default_width: f32) -> Self {
|
||||||
self.default_width = default_width;
|
self.default_width = default_width;
|
||||||
|
@ -212,7 +202,6 @@ impl SidePanel {
|
||||||
id,
|
id,
|
||||||
frame,
|
frame,
|
||||||
resizable,
|
resizable,
|
||||||
show_separator_line,
|
|
||||||
default_width,
|
default_width,
|
||||||
width_range,
|
width_range,
|
||||||
} = self;
|
} = self;
|
||||||
|
@ -245,12 +234,11 @@ 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;
|
||||||
|
|
||||||
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks
|
||||||
&& mouse_over_resize_line
|
if any_pressed && ui.input().pointer.any_down() && 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(|mem| mem.is_being_dragged(resize_id));
|
is_resizing = ui.memory().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 =
|
||||||
|
@ -258,12 +246,12 @@ impl SidePanel {
|
||||||
side.set_rect_width(&mut panel_rect, width);
|
side.set_rect_width(&mut panel_rect, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dragging_something_else =
|
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
||||||
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
let dragging_something_else = any_down || ui.input().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.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,20 +285,15 @@ impl SidePanel {
|
||||||
|
|
||||||
{
|
{
|
||||||
let stroke = if is_resizing {
|
let stroke = if is_resizing {
|
||||||
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
ui.style().visuals.widgets.active.bg_stroke
|
||||||
} else if resize_hover {
|
} else if resize_hover {
|
||||||
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
ui.style().visuals.widgets.hovered.bg_stroke
|
||||||
} else if show_separator_line {
|
|
||||||
// TOOD(emilk): distinguish resizable from non-resizable
|
|
||||||
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
// TOOD(emilk): distinguish resizable from non-resizable
|
||||||
|
ui.style().visuals.widgets.noninteractive.bg_stroke
|
||||||
};
|
};
|
||||||
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
||||||
// In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel
|
let resize_x = side.opposite().side_x(rect);
|
||||||
// (hence the shrink).
|
|
||||||
let resize_x = side.opposite().side_x(rect.shrink(1.0));
|
|
||||||
let resize_x = ui.painter().round_to_pixel(resize_x);
|
|
||||||
ui.painter().vline(resize_x, rect.y_range(), stroke);
|
ui.painter().vline(resize_x, rect.y_range(), stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,19 +318,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.screen_rect();
|
let clip_rect = ctx.input().screen_rect();
|
||||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
|
let 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.frame_state_mut(|state| {
|
Side::Left => ctx
|
||||||
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
|
.frame_state()
|
||||||
}),
|
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
|
||||||
Side::Right => ctx.frame_state_mut(|state| {
|
Side::Right => ctx
|
||||||
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
|
.frame_state()
|
||||||
}),
|
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
|
||||||
}
|
}
|
||||||
inner_response
|
inner_response
|
||||||
}
|
}
|
||||||
|
@ -366,8 +349,6 @@ impl SidePanel {
|
||||||
None
|
None
|
||||||
} else if how_expanded < 1.0 {
|
} else if how_expanded < 1.0 {
|
||||||
// Show a fake panel in this in-between animation state:
|
// Show a fake panel in this in-between animation state:
|
||||||
// TODO(emilk): move the panel out-of-screen instead of changing its width.
|
|
||||||
// Then we can actually paint it as it animates.
|
|
||||||
let expanded_width = PanelState::load(ctx, self.id)
|
let expanded_width = PanelState::load(ctx, self.id)
|
||||||
.map_or(self.default_width, |state| state.rect.width());
|
.map_or(self.default_width, |state| state.rect.width());
|
||||||
let fake_width = how_expanded * expanded_width;
|
let fake_width = how_expanded * expanded_width;
|
||||||
|
@ -401,8 +382,6 @@ impl SidePanel {
|
||||||
None
|
None
|
||||||
} else if how_expanded < 1.0 {
|
} else if how_expanded < 1.0 {
|
||||||
// Show a fake panel in this in-between animation state:
|
// Show a fake panel in this in-between animation state:
|
||||||
// TODO(emilk): move the panel out-of-screen instead of changing its width.
|
|
||||||
// Then we can actually paint it as it animates.
|
|
||||||
let expanded_width = PanelState::load(ui.ctx(), self.id)
|
let expanded_width = PanelState::load(ui.ctx(), self.id)
|
||||||
.map_or(self.default_width, |state| state.rect.width());
|
.map_or(self.default_width, |state| state.rect.width());
|
||||||
let fake_width = how_expanded * expanded_width;
|
let fake_width = how_expanded * expanded_width;
|
||||||
|
@ -489,7 +468,7 @@ impl SidePanel {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom)
|
/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom)
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum TopBottomSide {
|
pub enum TopBottomSide {
|
||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
|
@ -542,7 +521,6 @@ pub struct TopBottomPanel {
|
||||||
id: Id,
|
id: Id,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
resizable: bool,
|
resizable: bool,
|
||||||
show_separator_line: bool,
|
|
||||||
default_height: Option<f32>,
|
default_height: Option<f32>,
|
||||||
height_range: RangeInclusive<f32>,
|
height_range: RangeInclusive<f32>,
|
||||||
}
|
}
|
||||||
|
@ -565,7 +543,6 @@ impl TopBottomPanel {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
frame: None,
|
frame: None,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
show_separator_line: true,
|
|
||||||
default_height: None,
|
default_height: None,
|
||||||
height_range: 20.0..=f32::INFINITY,
|
height_range: 20.0..=f32::INFINITY,
|
||||||
}
|
}
|
||||||
|
@ -587,14 +564,6 @@ impl TopBottomPanel {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a separator line, even when not interacting with it?
|
|
||||||
///
|
|
||||||
/// Default: `true`.
|
|
||||||
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
|
||||||
self.show_separator_line = show_separator_line;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The initial height of the [`SidePanel`].
|
/// The initial height of the [`SidePanel`].
|
||||||
/// Defaults to [`style::Spacing::interact_size`].y.
|
/// Defaults to [`style::Spacing::interact_size`].y.
|
||||||
pub fn default_height(mut self, default_height: f32) -> Self {
|
pub fn default_height(mut self, default_height: f32) -> Self {
|
||||||
|
@ -660,7 +629,6 @@ impl TopBottomPanel {
|
||||||
id,
|
id,
|
||||||
frame,
|
frame,
|
||||||
resizable,
|
resizable,
|
||||||
show_separator_line,
|
|
||||||
default_height,
|
default_height,
|
||||||
height_range,
|
height_range,
|
||||||
} = self;
|
} = self;
|
||||||
|
@ -683,7 +651,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(|i| i.pointer.latest_pos());
|
let latest_pos = ui.input().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()
|
||||||
|
@ -696,12 +664,13 @@ 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(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
if ui.input().pointer.any_pressed()
|
||||||
|
&& ui.input().pointer.any_down()
|
||||||
&& mouse_over_resize_line
|
&& mouse_over_resize_line
|
||||||
{
|
{
|
||||||
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id));
|
ui.memory().interaction.drag_id = Some(resize_id);
|
||||||
}
|
}
|
||||||
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
|
is_resizing = ui.memory().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 +678,12 @@ impl TopBottomPanel {
|
||||||
side.set_rect_height(&mut panel_rect, height);
|
side.set_rect_height(&mut panel_rect, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dragging_something_else =
|
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
||||||
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
let dragging_something_else = any_down || ui.input().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.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
|
ui.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -748,20 +717,15 @@ impl TopBottomPanel {
|
||||||
|
|
||||||
{
|
{
|
||||||
let stroke = if is_resizing {
|
let stroke = if is_resizing {
|
||||||
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
ui.style().visuals.widgets.active.bg_stroke
|
||||||
} else if resize_hover {
|
} else if resize_hover {
|
||||||
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
ui.style().visuals.widgets.hovered.bg_stroke
|
||||||
} else if show_separator_line {
|
|
||||||
// TOOD(emilk): distinguish resizable from non-resizable
|
|
||||||
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
// TOOD(emilk): distinguish resizable from non-resizable
|
||||||
|
ui.style().visuals.widgets.noninteractive.bg_stroke
|
||||||
};
|
};
|
||||||
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
||||||
// In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel
|
let resize_y = side.opposite().side_y(rect);
|
||||||
// (hence the shrink).
|
|
||||||
let resize_y = side.opposite().side_y(rect.shrink(1.0));
|
|
||||||
let resize_y = ui.painter().round_to_pixel(resize_y);
|
|
||||||
ui.painter().hline(rect.x_range(), resize_y, stroke);
|
ui.painter().hline(rect.x_range(), resize_y, stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,7 +751,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.screen_rect();
|
let clip_rect = ctx.input().screen_rect();
|
||||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
|
let 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,14 +759,12 @@ impl TopBottomPanel {
|
||||||
|
|
||||||
match side {
|
match side {
|
||||||
TopBottomSide::Top => {
|
TopBottomSide::Top => {
|
||||||
ctx.frame_state_mut(|state| {
|
ctx.frame_state()
|
||||||
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
TopBottomSide::Bottom => {
|
TopBottomSide::Bottom => {
|
||||||
ctx.frame_state_mut(|state| {
|
ctx.frame_state()
|
||||||
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -823,8 +785,6 @@ impl TopBottomPanel {
|
||||||
None
|
None
|
||||||
} else if how_expanded < 1.0 {
|
} else if how_expanded < 1.0 {
|
||||||
// Show a fake panel in this in-between animation state:
|
// Show a fake panel in this in-between animation state:
|
||||||
// TODO(emilk): move the panel out-of-screen instead of changing its height.
|
|
||||||
// Then we can actually paint it as it animates.
|
|
||||||
let expanded_height = PanelState::load(ctx, self.id)
|
let expanded_height = PanelState::load(ctx, self.id)
|
||||||
.map(|state| state.rect.height())
|
.map(|state| state.rect.height())
|
||||||
.or(self.default_height)
|
.or(self.default_height)
|
||||||
|
@ -860,8 +820,6 @@ impl TopBottomPanel {
|
||||||
None
|
None
|
||||||
} else if how_expanded < 1.0 {
|
} else if how_expanded < 1.0 {
|
||||||
// Show a fake panel in this in-between animation state:
|
// Show a fake panel in this in-between animation state:
|
||||||
// TODO(emilk): move the panel out-of-screen instead of changing its height.
|
|
||||||
// Then we can actually paint it as it animates.
|
|
||||||
let expanded_height = PanelState::load(ui.ctx(), self.id)
|
let expanded_height = PanelState::load(ui.ctx(), self.id)
|
||||||
.map(|state| state.rect.height())
|
.map(|state| state.rect.height())
|
||||||
.or(self.default_height)
|
.or(self.default_height)
|
||||||
|
@ -1044,13 +1002,14 @@ 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.screen_rect();
|
let clip_rect = ctx.input().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_mut(|state| state.allocate_central_panel(inner_response.response.rect));
|
ctx.frame_state()
|
||||||
|
.allocate_central_panel(inner_response.response.rect);
|
||||||
|
|
||||||
inner_response
|
inner_response
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,16 @@ pub(crate) struct TooltipState {
|
||||||
|
|
||||||
impl TooltipState {
|
impl TooltipState {
|
||||||
pub fn load(ctx: &Context) -> Option<Self> {
|
pub fn load(ctx: &Context) -> Option<Self> {
|
||||||
ctx.data_mut(|d| d.get_temp(Id::null()))
|
ctx.data().get_temp(Id::null())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ctx: &Context) {
|
fn store(self, ctx: &Context) {
|
||||||
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
|
ctx.data().insert_temp(Id::null(), self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
||||||
if self.last_common_id == Some(common_id) {
|
if self.last_common_id == Some(common_id) {
|
||||||
Some(self.individual_ids_and_sizes.get(&index)?.1)
|
Some(self.individual_ids_and_sizes.get(&index).cloned()?.1)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,9 @@ 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(|i| i.pointer.hover_pos())
|
.input()
|
||||||
|
.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)
|
||||||
}
|
}
|
||||||
|
@ -110,7 +112,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(|i| i.any_touches()) {
|
let (above, position) = if ctx.input().any_touches() {
|
||||||
(true, expanded_rect.left_top())
|
(true, expanded_rect.left_top())
|
||||||
} else {
|
} else {
|
||||||
(false, expanded_rect.left_bottom())
|
(false, expanded_rect.left_bottom())
|
||||||
|
@ -157,7 +159,8 @@ 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(|fs| fs.tooltip_state)
|
ctx.frame_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,
|
||||||
|
@ -173,7 +176,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(|mem| mem.everything_is_visible()) {
|
} else if ctx.memory().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 :(
|
||||||
|
@ -188,7 +191,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
position.y -= expected_size.y;
|
position.y -= expected_size.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
position = position.at_most(ctx.screen_rect().max - expected_size);
|
position = position.at_most(ctx.input().screen_rect().max - expected_size);
|
||||||
|
|
||||||
// check if we intersect the avoid_rect
|
// check if we intersect the avoid_rect
|
||||||
{
|
{
|
||||||
|
@ -206,7 +209,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let position = position.at_least(ctx.screen_rect().min);
|
let position = position.at_least(ctx.input().screen_rect().min);
|
||||||
|
|
||||||
let area_id = frame_state.common_id.with(frame_state.count);
|
let area_id = frame_state.common_id.with(frame_state.count);
|
||||||
|
|
||||||
|
@ -223,7 +226,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_mut(|fs| fs.tooltip_state = Some(frame_state));
|
ctx.frame_state().tooltip_state = Some(frame_state);
|
||||||
|
|
||||||
Some(inner)
|
Some(inner)
|
||||||
}
|
}
|
||||||
|
@ -260,9 +263,8 @@ 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(ctx.screen_rect())
|
.drag_bounds(Rect::EVERYTHING) // disable clip rect
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
Frame::popup(&ctx.style())
|
Frame::popup(&ctx.style())
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
@ -281,7 +283,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(|mem| mem.areas.visible_last_frame(&layer_id)) {
|
if ctx.memory().areas.visible_last_frame(&layer_id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,8 +314,6 @@ 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_mut(|mem| mem.toggle_popup(popup_id));
|
/// ui.memory().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(|mem| mem.is_popup_open(popup_id)) {
|
if ui.memory().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(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
|
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
|
||||||
ui.memory_mut(|mem| mem.close_popup());
|
ui.memory().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_mut(|d| d.get_persisted(id))
|
ctx.data().get_persisted(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
ctx.data().insert_persisted(id, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ impl Resize {
|
||||||
.at_least(self.min_size)
|
.at_least(self.min_size)
|
||||||
.at_most(self.max_size)
|
.at_most(self.max_size)
|
||||||
.at_most(
|
.at_most(
|
||||||
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||||
);
|
);
|
||||||
|
|
||||||
State {
|
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().set_cursor_icon(CursorIcon::ResizeNwSe);
|
ui.ctx().output().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_mut(|d| d.get_persisted(id))
|
ctx.data().get_persisted(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
ctx.data().insert_persisted(id, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,10 +97,8 @@ pub struct ScrollArea {
|
||||||
id_source: Option<Id>,
|
id_source: Option<Id>,
|
||||||
offset_x: Option<f32>,
|
offset_x: Option<f32>,
|
||||||
offset_y: Option<f32>,
|
offset_y: Option<f32>,
|
||||||
|
|
||||||
/// If false, we ignore scroll events.
|
/// If false, we ignore scroll events.
|
||||||
scrolling_enabled: bool,
|
scrolling_enabled: bool,
|
||||||
drag_to_scroll: bool,
|
|
||||||
|
|
||||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||||
/// end position until user manually changes position. It will become true
|
/// end position until user manually changes position. It will become true
|
||||||
|
@ -143,7 +141,6 @@ impl ScrollArea {
|
||||||
offset_x: None,
|
offset_x: None,
|
||||||
offset_y: None,
|
offset_y: None,
|
||||||
scrolling_enabled: true,
|
scrolling_enabled: true,
|
||||||
drag_to_scroll: true,
|
|
||||||
stick_to_end: [false; 2],
|
stick_to_end: [false; 2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,13 +253,12 @@ impl ScrollArea {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Control the scrolling behavior.
|
/// Control the scrolling behavior
|
||||||
///
|
/// If `true` (default), the scroll area will respond to user scrolling
|
||||||
/// * If `true` (default), the scroll area will respond to user scrolling.
|
/// If `false`, the scroll area will not respond to user scrolling
|
||||||
/// * If `false`, the scroll area will not respond to user scrolling.
|
|
||||||
///
|
///
|
||||||
/// This can be used, for example, to optionally freeze scrolling while the user
|
/// This can be used, for example, to optionally freeze scrolling while the user
|
||||||
/// is typing text in a [`TextEdit`] widget contained within the scroll area.
|
/// is inputing text in a [`TextEdit`] widget contained within the scroll area.
|
||||||
///
|
///
|
||||||
/// This controls both scrolling directions.
|
/// This controls both scrolling directions.
|
||||||
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
||||||
|
@ -270,22 +266,10 @@ impl ScrollArea {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Can the user drag the scroll area to scroll?
|
|
||||||
///
|
|
||||||
/// This is useful for touch screens.
|
|
||||||
///
|
|
||||||
/// If `true`, the [`ScrollArea`] will sense drags.
|
|
||||||
///
|
|
||||||
/// Default: `true`.
|
|
||||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
|
||||||
self.drag_to_scroll = drag_to_scroll;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For each axis, should the containing area shrink if the content is small?
|
/// For each axis, should the containing area shrink if the content is small?
|
||||||
///
|
///
|
||||||
/// * If `true`, egui will add blank space outside the scroll area.
|
/// If true, egui will add blank space outside the scroll area.
|
||||||
/// * If `false`, egui will add blank space inside the scroll area.
|
/// If false, egui will add blank space inside the scroll area.
|
||||||
///
|
///
|
||||||
/// Default: `[true; 2]`.
|
/// Default: `[true; 2]`.
|
||||||
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
|
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
|
||||||
|
@ -351,7 +335,6 @@ impl ScrollArea {
|
||||||
offset_x,
|
offset_x,
|
||||||
offset_y,
|
offset_y,
|
||||||
scrolling_enabled,
|
scrolling_enabled,
|
||||||
drag_to_scroll,
|
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -426,40 +409,19 @@ 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());
|
||||||
// Clip the content, but only when we really need to:
|
|
||||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
|
||||||
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
|
|
||||||
let mut content_clip_rect = ui.clip_rect();
|
|
||||||
for d in 0..2 {
|
|
||||||
if has_bar[d] {
|
|
||||||
if state.content_is_too_large[d] {
|
|
||||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
|
||||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.show_scroll[d] {
|
|
||||||
// Make sure content doesn't cover scroll bars
|
|
||||||
let tiny_gap = 1.0;
|
|
||||||
content_clip_rect.max[1 - d] =
|
|
||||||
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Nice handling of forced resizing beyond the possible:
|
// Nice handling of forced resizing beyond the possible:
|
||||||
|
for d in 0..2 {
|
||||||
|
if !has_bar[d] {
|
||||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
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);
|
||||||
|
|
||||||
if (scrolling_enabled && drag_to_scroll)
|
if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) {
|
||||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
|
||||||
{
|
|
||||||
// Drag contents to scroll (for touch screens mostly).
|
// Drag contents to scroll (for touch screens mostly).
|
||||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||||
// or we will steal input from the widgets we contain.
|
// or we will steal input from the widgets we contain.
|
||||||
|
@ -468,10 +430,8 @@ 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] {
|
||||||
ui.input(|input| {
|
state.offset[d] -= ui.input().pointer.delta()[d];
|
||||||
state.offset[d] -= input.pointer.delta()[d];
|
state.vel[d] = ui.input().pointer.velocity()[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;
|
||||||
|
@ -480,7 +440,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(|i| i.unstable_dt);
|
let dt = ui.input().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 {
|
||||||
|
@ -624,9 +584,7 @@ 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
|
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
||||||
.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();
|
||||||
|
@ -691,7 +649,8 @@ 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 scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
|
let mut frame_state = ui.ctx().frame_state();
|
||||||
|
let scroll_delta = frame_state.scroll_delta;
|
||||||
|
|
||||||
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
|
let scrolling_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;
|
||||||
|
@ -699,7 +658,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.
|
||||||
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
|
frame_state.scroll_delta[d] = 0.0;
|
||||||
state.scroll_stuck_to_end[d] = false;
|
state.scroll_stuck_to_end[d] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -841,7 +800,7 @@ impl Prepared {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let min_handle_size = ui.spacing().scroll_handle_min_length;
|
let min_handle_size = ui.spacing().scroll_bar_width;
|
||||||
if handle_rect.size()[d] < min_handle_size {
|
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, collapsed/expanded
|
/// * default, minimum, maximum and/or fixed size
|
||||||
/// * 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,7 +30,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +51,6 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,18 +77,6 @@ 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 {
|
||||||
|
@ -176,12 +162,6 @@ 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);
|
||||||
|
@ -295,14 +275,12 @@ 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_explicitly_closed = matches!(open, Some(false));
|
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
|
||||||
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 {
|
||||||
|
@ -313,7 +291,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"), default_open);
|
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
|
||||||
|
|
||||||
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);
|
||||||
|
@ -340,7 +318,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();
|
||||||
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
|
title.font_height(&ctx.fonts(), &style) + title_content_spacing
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
@ -426,7 +404,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(|i| i.pointer.has_pointer()) {
|
if ctx.input().pointer.has_pointer() {
|
||||||
paint_frame_interaction(
|
paint_frame_interaction(
|
||||||
&mut area_content_ui,
|
&mut area_content_ui,
|
||||||
outer_rect,
|
outer_rect,
|
||||||
|
@ -438,12 +416,9 @@ impl<'open> Window<'open> {
|
||||||
content_inner
|
content_inner
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
area.state_mut().pos = ctx
|
||||||
let pos = ctx
|
|
||||||
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
||||||
.left_top();
|
.min;
|
||||||
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);
|
||||||
|
|
||||||
|
@ -524,13 +499,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.set_cursor_icon(CursorIcon::ResizeNwSe);
|
ctx.output().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.set_cursor_icon(CursorIcon::ResizeNeSw);
|
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
|
||||||
} else if self.left || self.right {
|
} else if self.left || self.right {
|
||||||
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
|
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||||
} else if self.bottom || self.top {
|
} else if self.bottom || self.top {
|
||||||
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
|
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,7 +528,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().set_left_top_pos(new_rect.left_top());
|
area.state_mut().pos = new_rect.min;
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -562,7 +537,7 @@ fn interact(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
|
ctx.memory().areas.move_to_top(area_layer_id);
|
||||||
Some(window_interaction)
|
Some(window_interaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -570,11 +545,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(|i| i.pointer.primary_down()) {
|
if !ctx.input().pointer.primary_down() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
let pointer_pos = ctx.input().pointer.interact_pos()?;
|
||||||
let mut rect = window_interaction.start_rect; // prevent drift
|
let mut rect = window_interaction.start_rect; // prevent drift
|
||||||
|
|
||||||
if window_interaction.is_resize() {
|
if window_interaction.is_resize() {
|
||||||
|
@ -596,8 +571,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(|i| i.pointer.any_pressed()) {
|
if !ctx.input().pointer.any_pressed() {
|
||||||
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
|
let press_origin = ctx.input().pointer.press_origin()?;
|
||||||
let delta = pointer_pos - press_origin;
|
let delta = pointer_pos - press_origin;
|
||||||
rect = rect.translate(delta);
|
rect = rect.translate(delta);
|
||||||
}
|
}
|
||||||
|
@ -615,31 +590,30 @@ fn window_interaction(
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
) -> Option<WindowInteraction> {
|
) -> Option<WindowInteraction> {
|
||||||
{
|
{
|
||||||
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
|
let drag_id = ctx.memory().interaction.drag_id;
|
||||||
|
|
||||||
if drag_id.is_some() && drag_id != Some(id) {
|
if drag_id.is_some() && drag_id != Some(id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
|
let mut window_interaction = { ctx.memory().window_interaction };
|
||||||
|
|
||||||
if window_interaction.is_none() {
|
if 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);
|
||||||
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
|
let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks
|
||||||
ctx.memory_mut(|mem| {
|
if any_pressed && ctx.input().pointer.primary_down() {
|
||||||
mem.interaction.drag_id = Some(id);
|
ctx.memory().interaction.drag_id = Some(id);
|
||||||
mem.interaction.drag_is_window = true;
|
ctx.memory().interaction.drag_is_window = true;
|
||||||
window_interaction = Some(hover_window_interaction);
|
window_interaction = Some(hover_window_interaction);
|
||||||
mem.window_interaction = window_interaction;
|
ctx.memory().window_interaction = window_interaction;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window_interaction) = window_interaction {
|
if let Some(window_interaction) = window_interaction {
|
||||||
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
|
let is_active = ctx.memory().interaction.drag_id == Some(id);
|
||||||
|
|
||||||
if is_active && window_interaction.area_layer_id == area_layer_id {
|
if is_active && window_interaction.area_layer_id == area_layer_id {
|
||||||
return Some(window_interaction);
|
return Some(window_interaction);
|
||||||
|
@ -655,9 +629,10 @@ fn resize_hover(
|
||||||
area_layer_id: LayerId,
|
area_layer_id: LayerId,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
) -> Option<WindowInteraction> {
|
) -> Option<WindowInteraction> {
|
||||||
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
|
let pointer = ctx.input().pointer.interact_pos()?;
|
||||||
|
|
||||||
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
|
let any_down = ctx.input().pointer.any_down(); // avoid deadlocks
|
||||||
|
if any_down && !ctx.input().pointer.any_pressed() {
|
||||||
return None; // already dragging (something)
|
return None; // already dragging (something)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,7 +642,7 @@ fn resize_hover(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.memory(|mem| mem.interaction.drag_interest) {
|
if ctx.memory().interaction.drag_interest {
|
||||||
// Another widget will become active if we drag here
|
// Another widget will become active if we drag here
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -829,8 +804,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 = ui
|
let height = title
|
||||||
.fonts(|fonts| title.font_height(fonts, ui.style()))
|
.font_height(&ui.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
|
@ -133,7 +133,7 @@ impl RawInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file about to be dropped into egui.
|
/// A file about to be dropped into egui.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct HoveredFile {
|
pub struct HoveredFile {
|
||||||
/// Set by the `egui-winit` backend.
|
/// Set by the `egui-winit` backend.
|
||||||
|
@ -144,7 +144,7 @@ pub struct HoveredFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A file dropped into egui.
|
/// A file dropped into egui.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct DroppedFile {
|
pub struct DroppedFile {
|
||||||
/// Set by the `egui-winit` backend.
|
/// Set by the `egui-winit` backend.
|
||||||
|
@ -187,15 +187,6 @@ pub enum Event {
|
||||||
/// Was it pressed or released?
|
/// Was it pressed or released?
|
||||||
pressed: bool,
|
pressed: bool,
|
||||||
|
|
||||||
/// If this is a `pressed` event, is it a key-repeat?
|
|
||||||
///
|
|
||||||
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
|
|
||||||
/// Sometimes you will want to ignore such events, and this lets you do that.
|
|
||||||
///
|
|
||||||
/// egui will automatically detect such repeat events and mark them as such here.
|
|
||||||
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
|
|
||||||
repeat: bool,
|
|
||||||
|
|
||||||
/// The state of the modifier keys at the time of the event.
|
/// The state of the modifier keys at the time of the event.
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
},
|
},
|
||||||
|
@ -277,10 +268,6 @@ pub enum Event {
|
||||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||||
force: f32,
|
force: f32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// An assistive technology (e.g. screen reader) requested an action.
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
AccessKitActionRequest(accesskit::ActionRequest),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mouse button (or similar for touch input)
|
/// Mouse button (or similar for touch input)
|
||||||
|
@ -314,7 +301,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, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[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 +764,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, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
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_mut(|o| o.copied_text = "some_text".to_string());
|
/// ui.output().copied_text = "some_text".to_string();
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -85,9 +85,6 @@ pub struct PlatformOutput {
|
||||||
|
|
||||||
/// Screen-space position of text edit cursor (used for IME).
|
/// Screen-space position of text edit cursor (used for IME).
|
||||||
pub text_cursor_pos: Option<crate::Pos2>,
|
pub text_cursor_pos: Option<crate::Pos2>,
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformOutput {
|
impl PlatformOutput {
|
||||||
|
@ -124,8 +121,6 @@ impl PlatformOutput {
|
||||||
mut events,
|
mut events,
|
||||||
mutable_text_under_cursor,
|
mutable_text_under_cursor,
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_update,
|
|
||||||
} = newer;
|
} = newer;
|
||||||
|
|
||||||
self.cursor_icon = cursor_icon;
|
self.cursor_icon = cursor_icon;
|
||||||
|
@ -138,13 +133,6 @@ impl PlatformOutput {
|
||||||
self.events.append(&mut events);
|
self.events.append(&mut events);
|
||||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||||
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
// egui produces a complete AccessKit tree for each frame,
|
|
||||||
// so overwrite rather than appending.
|
|
||||||
self.accesskit_update = accesskit_update;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
||||||
|
@ -156,7 +144,7 @@ impl PlatformOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What URL to open, and how.
|
/// What URL to open, and how.
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct OpenUrl {
|
pub struct OpenUrl {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -190,7 +178,7 @@ impl OpenUrl {
|
||||||
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
||||||
///
|
///
|
||||||
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
|
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum CursorIcon {
|
pub enum CursorIcon {
|
||||||
/// Normal cursor icon, whatever that is.
|
/// Normal cursor icon, whatever that is.
|
||||||
|
@ -384,19 +372,6 @@ pub enum OutputEvent {
|
||||||
ValueChanged(WidgetInfo),
|
ValueChanged(WidgetInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputEvent {
|
|
||||||
pub fn widget_info(&self) -> &WidgetInfo {
|
|
||||||
match self {
|
|
||||||
OutputEvent::Clicked(info)
|
|
||||||
| OutputEvent::DoubleClicked(info)
|
|
||||||
| OutputEvent::TripleClicked(info)
|
|
||||||
| OutputEvent::FocusGained(info)
|
|
||||||
| OutputEvent::TextSelectionChanged(info)
|
|
||||||
| OutputEvent::ValueChanged(info) => info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for OutputEvent {
|
impl std::fmt::Debug for OutputEvent {
|
||||||
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 {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use crate::{id::IdSet, *};
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct TooltipFrameState {
|
pub(crate) struct TooltipFrameState {
|
||||||
|
@ -9,13 +9,6 @@ pub(crate) struct TooltipFrameState {
|
||||||
pub count: usize,
|
pub count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct AccessKitFrameState {
|
|
||||||
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
|
|
||||||
pub(crate) parent_stack: Vec<Id>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State that is collected during a frame and then cleared.
|
/// State that is collected during a frame and then cleared.
|
||||||
/// Short-term (single frame) memory.
|
/// Short-term (single frame) memory.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -48,15 +41,6 @@ pub(crate) struct FrameState {
|
||||||
|
|
||||||
/// horizontal, vertical
|
/// horizontal, vertical
|
||||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
|
||||||
|
|
||||||
/// Highlight these widgets this next frame. Read from this.
|
|
||||||
pub(crate) highlight_this_frame: IdSet,
|
|
||||||
|
|
||||||
/// Highlight these widgets the next frame. Write to this.
|
|
||||||
pub(crate) highlight_next_frame: IdSet,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrameState {
|
impl Default for FrameState {
|
||||||
|
@ -69,10 +53,6 @@ impl Default for FrameState {
|
||||||
tooltip_state: None,
|
tooltip_state: None,
|
||||||
scroll_delta: Vec2::ZERO,
|
scroll_delta: Vec2::ZERO,
|
||||||
scroll_target: [None, None],
|
scroll_target: [None, None],
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_state: None,
|
|
||||||
highlight_this_frame: Default::default(),
|
|
||||||
highlight_next_frame: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,10 +67,6 @@ impl FrameState {
|
||||||
tooltip_state,
|
tooltip_state,
|
||||||
scroll_delta,
|
scroll_delta,
|
||||||
scroll_target,
|
scroll_target,
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
accesskit_state,
|
|
||||||
highlight_this_frame,
|
|
||||||
highlight_next_frame,
|
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
used_ids.clear();
|
used_ids.clear();
|
||||||
|
@ -100,13 +76,6 @@ 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")]
|
|
||||||
{
|
|
||||||
*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_mut(|d| d.get_temp(id))
|
ctx.data().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_mut(|d| d.insert_temp(id, self));
|
ctx.data().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) {
|
||||||
|
@ -51,9 +51,6 @@ pub(crate) struct GridLayout {
|
||||||
style: std::sync::Arc<Style>,
|
style: std::sync::Arc<Style>,
|
||||||
id: Id,
|
id: Id,
|
||||||
|
|
||||||
/// First frame (no previous know state).
|
|
||||||
is_first_frame: bool,
|
|
||||||
|
|
||||||
/// State previous frame (if any).
|
/// State previous frame (if any).
|
||||||
/// This can be used to predict future sizes of cells.
|
/// This can be used to predict future sizes of cells.
|
||||||
prev_state: State,
|
prev_state: State,
|
||||||
|
@ -74,9 +71,8 @@ pub(crate) struct GridLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GridLayout {
|
impl GridLayout {
|
||||||
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
pub(crate) fn new(ui: &Ui, id: Id) -> Self {
|
||||||
let is_first_frame = prev_state.is_none();
|
let prev_state = State::load(ui.ctx(), id).unwrap_or_default();
|
||||||
let prev_state = prev_state.unwrap_or_default();
|
|
||||||
|
|
||||||
// TODO(emilk): respect current layout
|
// TODO(emilk): respect current layout
|
||||||
|
|
||||||
|
@ -92,7 +88,6 @@ impl GridLayout {
|
||||||
ctx: ui.ctx().clone(),
|
ctx: ui.ctx().clone(),
|
||||||
style: ui.style().clone(),
|
style: ui.style().clone(),
|
||||||
id,
|
id,
|
||||||
is_first_frame,
|
|
||||||
prev_state,
|
prev_state,
|
||||||
curr_state: State::default(),
|
curr_state: State::default(),
|
||||||
initial_available,
|
initial_available,
|
||||||
|
@ -130,16 +125,7 @@ impl GridLayout {
|
||||||
let is_last_column = Some(self.col + 1) == self.num_columns;
|
let is_last_column = Some(self.col + 1) == self.num_columns;
|
||||||
|
|
||||||
let width = if is_last_column {
|
let width = if is_last_column {
|
||||||
// The first frame we don't really know the widths of the previous columns,
|
(self.initial_available.right() - region.cursor.left()).at_most(self.max_cell_size.x)
|
||||||
// so returning a big available width here can cause trouble.
|
|
||||||
if self.is_first_frame {
|
|
||||||
self.curr_state
|
|
||||||
.col_width(self.col)
|
|
||||||
.unwrap_or(self.min_cell_size.x)
|
|
||||||
} else {
|
|
||||||
(self.initial_available.right() - region.cursor.left())
|
|
||||||
.at_most(self.max_cell_size.x)
|
|
||||||
}
|
|
||||||
} else if self.max_cell_size.x.is_finite() {
|
} else if self.max_cell_size.x.is_finite() {
|
||||||
// TODO(emilk): should probably heed `prev_state` here too
|
// TODO(emilk): should probably heed `prev_state` here too
|
||||||
self.max_cell_size.x
|
self.max_cell_size.x
|
||||||
|
@ -278,7 +264,7 @@ impl GridLayout {
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
id_source: Id,
|
id_source: Id,
|
||||||
num_columns: Option<usize>,
|
num_columns: Option<usize>,
|
||||||
striped: Option<bool>,
|
striped: bool,
|
||||||
min_col_width: Option<f32>,
|
min_col_width: Option<f32>,
|
||||||
min_row_height: Option<f32>,
|
min_row_height: Option<f32>,
|
||||||
max_cell_size: Vec2,
|
max_cell_size: Vec2,
|
||||||
|
@ -292,7 +278,7 @@ impl Grid {
|
||||||
Self {
|
Self {
|
||||||
id_source: Id::new(id_source),
|
id_source: Id::new(id_source),
|
||||||
num_columns: None,
|
num_columns: None,
|
||||||
striped: None,
|
striped: false,
|
||||||
min_col_width: None,
|
min_col_width: None,
|
||||||
min_row_height: None,
|
min_row_height: None,
|
||||||
max_cell_size: Vec2::INFINITY,
|
max_cell_size: Vec2::INFINITY,
|
||||||
|
@ -310,9 +296,9 @@ impl Grid {
|
||||||
/// If `true`, add a subtle background color to every other row.
|
/// If `true`, add a subtle background color to every other row.
|
||||||
///
|
///
|
||||||
/// This can make a table easier to read.
|
/// This can make a table easier to read.
|
||||||
/// Default is whatever is in [`crate::Visuals::striped`].
|
/// Default: `false`.
|
||||||
pub fn striped(mut self, striped: bool) -> Self {
|
pub fn striped(mut self, striped: bool) -> Self {
|
||||||
self.striped = Some(striped);
|
self.striped = striped;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,22 +357,18 @@ impl Grid {
|
||||||
spacing,
|
spacing,
|
||||||
start_row,
|
start_row,
|
||||||
} = self;
|
} = self;
|
||||||
let striped = striped.unwrap_or(ui.visuals().striped);
|
|
||||||
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
||||||
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||||
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
|
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
|
||||||
|
|
||||||
let id = ui.make_persistent_id(id_source);
|
|
||||||
let prev_state = State::load(ui.ctx(), id);
|
|
||||||
|
|
||||||
// Each grid cell is aligned LEFT_CENTER.
|
// Each grid cell is aligned LEFT_CENTER.
|
||||||
// If somebody wants to wrap more things inside a cell,
|
// If somebody wants to wrap more things inside a cell,
|
||||||
// then we should pick a default layout that matches that alignment,
|
// then we should pick a default layout that matches that alignment,
|
||||||
// which we do here:
|
// which we do here:
|
||||||
let max_rect = ui.cursor().intersect(ui.max_rect());
|
let max_rect = ui.cursor().intersect(ui.max_rect());
|
||||||
ui.allocate_ui_at_rect(max_rect, |ui| {
|
ui.allocate_ui_at_rect(max_rect, |ui| {
|
||||||
ui.set_visible(prev_state.is_some()); // Avoid visible first-frame jitter
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
let id = ui.make_persistent_id(id_source);
|
||||||
let grid = GridLayout {
|
let grid = GridLayout {
|
||||||
num_columns,
|
num_columns,
|
||||||
striped,
|
striped,
|
||||||
|
@ -394,7 +376,7 @@ impl Grid {
|
||||||
max_cell_size,
|
max_cell_size,
|
||||||
spacing,
|
spacing,
|
||||||
row: start_row,
|
row: start_row,
|
||||||
..GridLayout::new(ui, id, prev_state)
|
..GridLayout::new(ui, id)
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.set_grid(grid);
|
ui.set_grid(grid);
|
||||||
|
|
|
@ -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(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
|
if ctx.input_mut().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(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
|
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
|
||||||
zoom_in(ctx);
|
zoom_in(ctx);
|
||||||
}
|
}
|
||||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
|
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
|
||||||
zoom_out(ctx);
|
zoom_out(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,15 +65,11 @@ impl Id {
|
||||||
format!("{:04X}", self.0 as u16)
|
format!("{:04X}", self.0 as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The contained value.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub(crate) fn value(&self) -> u64 {
|
pub fn value(&self) -> u64 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
|
|
||||||
std::num::NonZeroU64::new(self.0).unwrap().into()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Id {
|
impl std::fmt::Debug for Id {
|
||||||
|
@ -168,8 +164,5 @@ 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>;
|
||||||
|
|
|
@ -164,31 +164,26 @@ impl InputState {
|
||||||
let mut keys_down = self.keys_down;
|
let mut keys_down = self.keys_down;
|
||||||
let mut scroll_delta = Vec2::ZERO;
|
let mut scroll_delta = Vec2::ZERO;
|
||||||
let mut zoom_factor_delta = 1.0;
|
let mut zoom_factor_delta = 1.0;
|
||||||
for event in &mut new.events {
|
new.events.retain(|event| match event {
|
||||||
match event {
|
Event::Key { key, pressed, .. } => {
|
||||||
Event::Key {
|
|
||||||
key,
|
|
||||||
pressed,
|
|
||||||
repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if *pressed {
|
if *pressed {
|
||||||
let first_press = keys_down.insert(*key);
|
// We only retain presses that are novel (i.e. the first Press event, not those generated by key-repeat)
|
||||||
*repeat = !first_press;
|
keys_down.insert(*key)
|
||||||
} else {
|
} else {
|
||||||
keys_down.remove(key);
|
keys_down.remove(key);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Scroll(delta) => {
|
Event::Scroll(delta) => {
|
||||||
scroll_delta += *delta;
|
scroll_delta += *delta;
|
||||||
|
true
|
||||||
}
|
}
|
||||||
Event::Zoom(factor) => {
|
Event::Zoom(factor) => {
|
||||||
zoom_factor_delta *= *factor;
|
zoom_factor_delta *= *factor;
|
||||||
|
true
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => true,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
InputState {
|
InputState {
|
||||||
pointer,
|
pointer,
|
||||||
touch_states: self.touch_states,
|
touch_states: self.touch_states,
|
||||||
|
@ -257,8 +252,6 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
|
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
|
||||||
///
|
|
||||||
/// Includes key-repeat events.
|
|
||||||
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
|
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
|
|
||||||
|
@ -268,8 +261,7 @@ impl InputState {
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: ev_key,
|
key: ev_key,
|
||||||
modifiers: ev_mods,
|
modifiers: ev_mods,
|
||||||
pressed: true,
|
pressed: true
|
||||||
..
|
|
||||||
} if *ev_key == key && ev_mods.matches(modifiers)
|
} if *ev_key == key && ev_mods.matches(modifiers)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -282,8 +274,6 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||||
///
|
|
||||||
/// Includes key-repeat events.
|
|
||||||
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
||||||
self.count_and_consume_key(modifiers, key) > 0
|
self.count_and_consume_key(modifiers, key) > 0
|
||||||
}
|
}
|
||||||
|
@ -291,31 +281,28 @@ impl InputState {
|
||||||
/// Check if the given shortcut has been pressed.
|
/// Check if the given shortcut has been pressed.
|
||||||
///
|
///
|
||||||
/// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
/// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||||
///
|
|
||||||
/// Includes key-repeat events.
|
|
||||||
pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool {
|
pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool {
|
||||||
let KeyboardShortcut { modifiers, key } = *shortcut;
|
let KeyboardShortcut { modifiers, key } = *shortcut;
|
||||||
self.consume_key(modifiers, key)
|
self.consume_key(modifiers, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the given key pressed this frame?
|
/// Was the given key pressed this frame?
|
||||||
///
|
|
||||||
/// Includes key-repeat events.
|
|
||||||
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
||||||
self.num_presses(desired_key) > 0
|
self.num_presses(desired_key) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many times was the given key pressed this frame?
|
/// How many times was the given key pressed this frame?
|
||||||
///
|
|
||||||
/// Includes key-repeat events.
|
|
||||||
pub fn num_presses(&self, desired_key: Key) -> usize {
|
pub fn num_presses(&self, desired_key: Key) -> usize {
|
||||||
self.events
|
self.events
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
matches!(
|
matches!(
|
||||||
event,
|
event,
|
||||||
Event::Key { key, pressed: true, .. }
|
Event::Key {
|
||||||
if *key == desired_key
|
key,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} if *key == desired_key
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.count()
|
.count()
|
||||||
|
@ -368,7 +355,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(|i| i.multi_touch());
|
/// let multi_touch = ui.input().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;
|
||||||
|
@ -412,33 +399,6 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn accesskit_action_requests(
|
|
||||||
&self,
|
|
||||||
id: crate::Id,
|
|
||||||
action: accesskit::Action,
|
|
||||||
) -> impl Iterator<Item = &accesskit::ActionRequest> {
|
|
||||||
let accesskit_id = id.accesskit_id();
|
|
||||||
self.events.iter().filter_map(move |event| {
|
|
||||||
if let Event::AccessKitActionRequest(request) = event {
|
|
||||||
if request.target == accesskit_id && request.action == action {
|
|
||||||
return Some(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
|
||||||
self.accesskit_action_requests(id, action).next().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
|
||||||
self.accesskit_action_requests(id, action).count()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -447,10 +407,9 @@ 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,
|
||||||
}
|
}
|
||||||
|
@ -472,10 +431,7 @@ pub(crate) enum PointerEvent {
|
||||||
position: Pos2,
|
position: Pos2,
|
||||||
button: PointerButton,
|
button: PointerButton,
|
||||||
},
|
},
|
||||||
Released {
|
Released(Option<Click>),
|
||||||
click: Option<Click>,
|
|
||||||
button: PointerButton,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PointerEvent {
|
impl PointerEvent {
|
||||||
|
@ -484,11 +440,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 { click: Some(_), .. })
|
matches!(self, PointerEvent::Released(Some(_click)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -643,6 +599,7 @@ impl PointerState {
|
||||||
|
|
||||||
Some(Click {
|
Some(Click {
|
||||||
pos,
|
pos,
|
||||||
|
button,
|
||||||
count,
|
count,
|
||||||
modifiers,
|
modifiers,
|
||||||
})
|
})
|
||||||
|
@ -650,8 +607,7 @@ impl PointerState {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pointer_events
|
self.pointer_events.push(PointerEvent::Released(click));
|
||||||
.push(PointerEvent::Released { click, button });
|
|
||||||
|
|
||||||
self.press_origin = None;
|
self.press_origin = None;
|
||||||
self.press_start_time = None;
|
self.press_start_time = None;
|
||||||
|
@ -779,28 +735,11 @@ 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{button: b, ..} if button == b))
|
.any(|event| matches!(event, &PointerEvent::Released(Some(Click{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?
|
||||||
|
@ -832,28 +771,16 @@ 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.iter().any(|event| {
|
self.pointer_events
|
||||||
matches!(
|
.iter()
|
||||||
&event,
|
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
|
||||||
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.iter().any(|event| {
|
self.pointer_events
|
||||||
matches!(
|
.iter()
|
||||||
&event,
|
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
|
||||||
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?
|
||||||
|
@ -866,6 +793,18 @@ 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,14 +96,10 @@ 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(|f| f.families());
|
let families = ui.fonts().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(|f| f.families());
|
let families = ui.fonts().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,7 +39,6 @@ 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 {
|
||||||
|
@ -110,7 +109,7 @@ impl LayerId {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
|
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub struct ShapeIdx(usize);
|
pub struct ShapeIdx(usize);
|
||||||
|
|
||||||
/// A list of [`Shape`]s paired with a clip rectangle.
|
/// A list of [`Shape`]s paired with a clip rectangle.
|
||||||
|
|
|
@ -75,7 +75,7 @@ impl Region {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
|
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
LeftToRight,
|
LeftToRight,
|
||||||
|
@ -114,7 +114,7 @@ impl Direction {
|
||||||
/// });
|
/// });
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[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 Layout {
|
pub struct Layout {
|
||||||
/// Main axis direction
|
/// Main axis direction
|
||||||
|
@ -229,10 +229,22 @@ impl Layout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For when you want to add a single widget to a layout, and that widget
|
||||||
|
/// should be centered horizontally and vertically.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn centered(main_dir: Direction) -> Self {
|
||||||
|
Self {
|
||||||
|
main_dir,
|
||||||
|
main_wrap: false,
|
||||||
|
main_align: Align::Center,
|
||||||
|
main_justify: false,
|
||||||
|
cross_align: Align::Center,
|
||||||
|
cross_justify: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// For when you want to add a single widget to a layout, and that widget
|
/// For when you want to add a single widget to a layout, and that widget
|
||||||
/// should use up all available space.
|
/// should use up all available space.
|
||||||
///
|
|
||||||
/// Only one widget may be added to the inner `Ui`!
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn centered_and_justified(main_dir: Direction) -> Self {
|
pub fn centered_and_justified(main_dir: Direction) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -324,23 +324,18 @@ pub mod util;
|
||||||
pub mod widget_text;
|
pub mod widget_text;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub use accesskit;
|
|
||||||
|
|
||||||
pub use epaint;
|
pub use epaint;
|
||||||
pub use epaint::ecolor;
|
|
||||||
pub use epaint::emath;
|
pub use epaint::emath;
|
||||||
|
|
||||||
#[cfg(feature = "color-hex")]
|
|
||||||
pub use ecolor::hex_color;
|
|
||||||
pub use ecolor::{Color32, Rgba};
|
|
||||||
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
|
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
|
||||||
|
#[cfg(feature = "color-hex")]
|
||||||
|
pub use epaint::hex_color;
|
||||||
pub use epaint::{
|
pub use epaint::{
|
||||||
mutex,
|
color, mutex,
|
||||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||||
textures::{TextureFilter, TextureOptions, TexturesDelta},
|
textures::{TextureFilter, TextureOptions, TexturesDelta},
|
||||||
ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo,
|
ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback,
|
||||||
Rounding, Shape, Stroke, TextureHandle, TextureId,
|
PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod text {
|
pub mod text {
|
||||||
|
@ -363,11 +358,11 @@ pub use {
|
||||||
input_state::{InputState, MultiTouchInfo, PointerState},
|
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||||
layers::{LayerId, Order},
|
layers::{LayerId, Order},
|
||||||
layout::*,
|
layout::*,
|
||||||
memory::{Memory, Options},
|
memory::Memory,
|
||||||
painter::Painter,
|
painter::Painter,
|
||||||
response::{InnerResponse, Response},
|
response::{InnerResponse, Response},
|
||||||
sense::Sense,
|
sense::Sense,
|
||||||
style::{FontSelection, Margin, Style, TextStyle, Visuals},
|
style::{FontSelection, Style, TextStyle, Visuals},
|
||||||
text::{Galley, TextFormat},
|
text::{Galley, TextFormat},
|
||||||
ui::Ui,
|
ui::Ui,
|
||||||
widget_text::{RichText, WidgetText},
|
widget_text::{RichText, WidgetText},
|
||||||
|
@ -509,34 +504,22 @@ pub mod special_emojis {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The different types of built-in widgets in egui
|
/// The different types of built-in widgets in egui
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[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.
|
||||||
|
@ -566,8 +549,3 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn accesskit_root_id() -> Id {
|
|
||||||
Id::new("accesskit_root")
|
|
||||||
}
|
|
||||||
|
|
|
@ -52,10 +52,9 @@ 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();
|
||||||
/// ctx.memory_mut(|mem| {
|
/// let mut memory = ctx.memory();
|
||||||
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
|
/// let cache = memory.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,
|
||||||
|
@ -103,15 +102,9 @@ pub struct Options {
|
||||||
/// Controls the tessellator.
|
/// Controls the tessellator.
|
||||||
pub tessellation_options: epaint::TessellationOptions,
|
pub tessellation_options: epaint::TessellationOptions,
|
||||||
|
|
||||||
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
/// This does not at all change the behavior of egui,
|
||||||
///
|
/// 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.
|
||||||
|
@ -173,7 +166,7 @@ pub(crate) struct Interaction {
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct Focus {
|
pub(crate) struct Focus {
|
||||||
/// The widget with keyboard focus (i.e. a text input field).
|
/// The widget with keyboard focus (i.e. a text input field).
|
||||||
pub(crate) id: Option<Id>,
|
id: Option<Id>,
|
||||||
|
|
||||||
/// What had keyboard focus previous frame?
|
/// What had keyboard focus previous frame?
|
||||||
id_previous_frame: Option<Id>,
|
id_previous_frame: Option<Id>,
|
||||||
|
@ -181,9 +174,6 @@ pub(crate) struct Focus {
|
||||||
/// Give focus to this widget next frame
|
/// Give focus to this widget next frame
|
||||||
id_next_frame: Option<Id>,
|
id_next_frame: Option<Id>,
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
|
||||||
|
|
||||||
/// If set, the next widget that is interested in focus will automatically get it.
|
/// If set, the next widget that is interested in focus will automatically get it.
|
||||||
/// Probably because the user pressed Tab.
|
/// Probably because the user pressed Tab.
|
||||||
give_to_next: bool,
|
give_to_next: bool,
|
||||||
|
@ -241,11 +231,6 @@ impl Focus {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
self.id_requested_by_accesskit = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pressed_tab = false;
|
self.pressed_tab = false;
|
||||||
self.pressed_shift_tab = false;
|
self.pressed_shift_tab = false;
|
||||||
for event in &new_input.events {
|
for event in &new_input.events {
|
||||||
|
@ -255,7 +240,6 @@ impl Focus {
|
||||||
key: crate::Key::Escape,
|
key: crate::Key::Escape,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers: _,
|
modifiers: _,
|
||||||
..
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
self.id = None;
|
self.id = None;
|
||||||
|
@ -267,7 +251,6 @@ impl Focus {
|
||||||
key: crate::Key::Tab,
|
key: crate::Key::Tab,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
if !self.is_focus_locked {
|
if !self.is_focus_locked {
|
||||||
|
@ -278,18 +261,6 @@ impl Focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
|
||||||
action: accesskit::Action::Focus,
|
|
||||||
target,
|
|
||||||
data: None,
|
|
||||||
}) = event
|
|
||||||
{
|
|
||||||
self.id_requested_by_accesskit = Some(*target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,17 +281,6 @@ impl Focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interested_in_focus(&mut self, id: Id) {
|
fn interested_in_focus(&mut self, id: Id) {
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
|
||||||
self.id = Some(id);
|
|
||||||
self.id_requested_by_accesskit = None;
|
|
||||||
self.give_to_next = false;
|
|
||||||
self.pressed_tab = false;
|
|
||||||
self.pressed_shift_tab = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.give_to_next && !self.had_focus_last_frame(id) {
|
if self.give_to_next && !self.had_focus_last_frame(id) {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self.give_to_next = false;
|
self.give_to_next = false;
|
||||||
|
@ -412,7 +372,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(&self, id: Id) -> bool {
|
pub fn has_lock_focus(&mut 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 {
|
||||||
|
@ -486,10 +446,6 @@ impl Memory {
|
||||||
self.popup == Some(popup_id) || self.everything_is_visible()
|
self.popup == Some(popup_id) || self.everything_is_visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn any_popup_open(&self) -> bool {
|
|
||||||
self.popup.is_some() || self.everything_is_visible()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_popup(&mut self, popup_id: Id) {
|
pub fn open_popup(&mut self, popup_id: Id) {
|
||||||
self.popup = Some(popup_id);
|
self.popup = Some(popup_id);
|
||||||
}
|
}
|
||||||
|
@ -600,8 +556,8 @@ impl Areas {
|
||||||
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
||||||
self.visible_last_frame
|
self.visible_last_frame
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.cloned()
|
||||||
.chain(self.visible_current_frame.iter().copied())
|
.chain(self.visible_current_frame.iter().cloned())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
|
ctx.data().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_mut(|d| d.insert_temp(bar_id, self));
|
ctx.data().insert_temp(bar_id, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a menu at pointer if primary-clicked response.
|
/// Show a menu at pointer if primary-clicked response.
|
||||||
|
@ -66,10 +66,10 @@ impl std::ops::DerefMut for BarState {
|
||||||
|
|
||||||
fn set_menu_style(style: &mut Style) {
|
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.weak_bg_fill = Color32::TRANSPARENT;
|
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
||||||
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
style.visuals.widgets.inactive.bg_stroke = Stroke::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The menu bar goes well in a [`TopBottomPanel::top`],
|
/// The menu bar goes well in a [`TopBottomPanel::top`],
|
||||||
|
@ -100,20 +100,6 @@ 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.
|
||||||
|
@ -143,10 +129,9 @@ 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(ctx.screen_rect());
|
.drag_bounds(Rect::EVERYTHING);
|
||||||
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());
|
||||||
|
|
||||||
|
@ -181,7 +166,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.weak_bg_fill);
|
button = button.fill(ui.visuals().widgets.open.bg_fill);
|
||||||
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,25 +177,6 @@ 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,
|
||||||
|
@ -311,9 +277,10 @@ impl MenuRoot {
|
||||||
root: &mut MenuRootManager,
|
root: &mut MenuRootManager,
|
||||||
id: Id,
|
id: Id,
|
||||||
) -> MenuResponse {
|
) -> MenuResponse {
|
||||||
if (response.clicked() && root.is_menu_open(id))
|
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
|
||||||
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
|
let input = response.ctx.input();
|
||||||
{
|
|
||||||
|
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))
|
||||||
|
@ -321,10 +288,12 @@ 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(|i| i.screen_rect);
|
let screen_rect = response.ctx.input().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();
|
||||||
|
@ -336,11 +305,8 @@ impl MenuRoot {
|
||||||
}
|
}
|
||||||
|
|
||||||
return MenuResponse::Create(pos, id);
|
return MenuResponse::Create(pos, id);
|
||||||
} else if response
|
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
|
||||||
.ctx
|
if let Some(pos) = input.pointer.interact_pos() {
|
||||||
.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
|
||||||
|
@ -363,8 +329,7 @@ impl MenuRoot {
|
||||||
id: Id,
|
id: Id,
|
||||||
) -> MenuResponse {
|
) -> MenuResponse {
|
||||||
let response = response.interact(Sense::click());
|
let response = response.interact(Sense::click());
|
||||||
response.ctx.input(|input| {
|
let pointer = &response.ctx.input().pointer;
|
||||||
let pointer = &input.pointer;
|
|
||||||
if pointer.any_pressed() {
|
if pointer.any_pressed() {
|
||||||
if let Some(pos) = pointer.interact_pos() {
|
if let Some(pos) = pointer.interact_pos() {
|
||||||
let mut destroy = false;
|
let mut destroy = false;
|
||||||
|
@ -384,7 +349,6 @@ impl MenuRoot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuResponse::Stay
|
MenuResponse::Stay
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
||||||
|
@ -446,7 +410,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.open
|
&ui.style().visuals.widgets.hovered
|
||||||
} else {
|
} else {
|
||||||
ui.style().interact(response)
|
ui.style().interact(response)
|
||||||
}
|
}
|
||||||
|
@ -475,8 +439,7 @@ 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 mut desired_size = text_and_icon_size + 2.0 * button_padding;
|
let 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(|| {
|
||||||
|
@ -496,7 +459,7 @@ impl SubMenuButton {
|
||||||
ui.painter().rect_filled(
|
ui.painter().rect_filled(
|
||||||
rect.expand(visuals.expansion),
|
rect.expand(visuals.expansion),
|
||||||
visuals.rounding,
|
visuals.rounding,
|
||||||
visuals.weak_bg_fill,
|
visuals.bg_fill,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,15 +571,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(|i| i.pointer.clone());
|
let pointer = &ui.input().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,6 +7,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
@ -104,12 +105,10 @@ impl Painter {
|
||||||
&self.ctx
|
&self.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read-only access to the shared [`Fonts`].
|
/// Available fonts.
|
||||||
///
|
|
||||||
/// See [`Context`] documentation for how locks work.
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
||||||
self.ctx.fonts(reader)
|
self.ctx.fonts()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where we paint
|
/// Where we paint
|
||||||
|
@ -153,9 +152,8 @@ impl Painter {
|
||||||
|
|
||||||
/// ## Low level
|
/// ## Low level
|
||||||
impl Painter {
|
impl Painter {
|
||||||
#[inline]
|
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
|
||||||
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
|
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
|
||||||
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) {
|
||||||
|
@ -169,11 +167,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(|l| l.add(self.clip_rect, Shape::Noop))
|
self.paint_list().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(|l| l.add(self.clip_rect, shape))
|
self.paint_list().add(self.clip_rect, shape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,9 +187,9 @@ impl Painter {
|
||||||
self.transform_shape(&mut shape);
|
self.transform_shape(&mut shape);
|
||||||
shape
|
shape
|
||||||
});
|
});
|
||||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
self.paint_list().extend(self.clip_rect, shapes);
|
||||||
} else {
|
} else {
|
||||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
self.paint_list().extend(self.clip_rect, shapes);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +200,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(|l| l.set(idx, self.clip_rect, shape));
|
self.paint_list().set(idx, self.clip_rect, shape);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,7 +405,7 @@ impl Painter {
|
||||||
color: crate::Color32,
|
color: crate::Color32,
|
||||||
wrap_width: f32,
|
wrap_width: f32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
|
self.fonts().layout(text, font_id, color, wrap_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will line break at `\n`.
|
/// Will line break at `\n`.
|
||||||
|
@ -420,7 +418,7 @@ impl Painter {
|
||||||
font_id: FontId,
|
font_id: FontId,
|
||||||
color: crate::Color32,
|
color: crate::Color32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
|
self.fonts().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`].
|
||||||
|
@ -450,6 +448,6 @@ impl Painter {
|
||||||
|
|
||||||
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
|
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
|
||||||
epaint::shape_transform::adjust_colors(shape, &|color| {
|
epaint::shape_transform::adjust_colors(shape, &|color| {
|
||||||
*color = crate::ecolor::tint_color_towards(*color, target);
|
*color = crate::color::tint_color_towards(*color, target);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ 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:
|
||||||
|
@ -43,10 +42,6 @@ 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],
|
||||||
|
@ -57,7 +52,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 triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||||
|
|
||||||
/// The widgets is being dragged
|
/// The widgets is being dragged
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -95,7 +90,6 @@ impl std::fmt::Debug for Response {
|
||||||
sense,
|
sense,
|
||||||
enabled,
|
enabled,
|
||||||
hovered,
|
hovered,
|
||||||
highlighted,
|
|
||||||
clicked,
|
clicked,
|
||||||
double_clicked,
|
double_clicked,
|
||||||
triple_clicked,
|
triple_clicked,
|
||||||
|
@ -112,7 +106,6 @@ 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)
|
||||||
|
@ -180,8 +173,7 @@ 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.
|
||||||
self.ctx.input(|i| {
|
let pointer = &self.ctx.input().pointer;
|
||||||
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
|
||||||
|
@ -198,7 +190,6 @@ impl Response {
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the widget enabled?
|
/// Was the widget enabled?
|
||||||
|
@ -220,24 +211,20 @@ 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 {
|
||||||
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
|
// Access input and memory in separate statements to prevent deadlock.
|
||||||
|
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(|mem| mem.gained_focus(self.id))
|
self.ctx.memory().gained_focus(self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The widget had keyboard focus and lost it,
|
/// The widget had keyboard focus and lost it,
|
||||||
|
@ -249,29 +236,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(|i| i.key_pressed(egui::Key::Enter)) {
|
/// if response.lost_focus() && ui.input().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(|mem| mem.lost_focus(self.id))
|
self.ctx.memory().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_mut(|mem| mem.request_focus(self.id));
|
self.ctx.memory().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_mut(|mem| mem.surrender_focus(self.id));
|
self.ctx.memory().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(|i| i.pointer.button_down(…))`).
|
/// (`ui.input().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`]).
|
||||||
|
@ -283,17 +270,12 @@ impl Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
||||||
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
|
self.dragged() && self.ctx.input().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(|i| i.pointer.any_pressed())
|
self.dragged && self.ctx.input().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.
|
||||||
|
@ -301,15 +283,10 @@ 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(|i| i.pointer.delta())
|
self.ctx.input().pointer.delta()
|
||||||
} else {
|
} else {
|
||||||
Vec2::ZERO
|
Vec2::ZERO
|
||||||
}
|
}
|
||||||
|
@ -325,7 +302,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(|i| i.pointer.hover_pos())
|
self.ctx.input().pointer.hover_pos()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -415,11 +392,11 @@ impl Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_show_hover_ui(&self) -> bool {
|
fn should_show_hover_ui(&self) -> bool {
|
||||||
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
if self.ctx.memory().everything_is_visible() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,7 +404,8 @@ 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.
|
||||||
|
|
||||||
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
|
let is_pointer_still = self.ctx.input().pointer.is_still();
|
||||||
|
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;
|
||||||
|
@ -436,9 +414,8 @@ 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
|
if self.ctx.input().pointer.any_down()
|
||||||
.ctx
|
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
|
||||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -467,17 +444,6 @@ 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| {
|
||||||
|
@ -488,15 +454,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.set_cursor_icon(cursor);
|
self.ctx.output().cursor_icon = cursor;
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When hovered or dragged, use this icon for the mouse cursor.
|
|
||||||
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
|
|
||||||
if self.hovered() || self.dragged() {
|
|
||||||
self.ctx.set_cursor_icon(cursor);
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -545,10 +503,8 @@ impl Response {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scroll_to_me(&self, align: Option<Align>) {
|
pub fn scroll_to_me(&self, align: Option<Align>) {
|
||||||
self.ctx.frame_state_mut(|state| {
|
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
|
||||||
state.scroll_target[0] = Some((self.rect.x_range(), align));
|
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
|
||||||
state.scroll_target[1] = Some((self.rect.y_range(), align));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For accessibility.
|
/// For accessibility.
|
||||||
|
@ -570,109 +526,10 @@ impl Response {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
self.output_event(event);
|
self.ctx.output().events.push(event);
|
||||||
} else {
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
|
||||||
self.fill_accesskit_node_from_widget_info(builder, make_info());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
|
||||||
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
|
|
||||||
});
|
|
||||||
self.ctx.output_mut(|o| o.events.push(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
|
|
||||||
builder.set_bounds(accesskit::Rect {
|
|
||||||
x0: self.rect.min.x.into(),
|
|
||||||
y0: self.rect.min.y.into(),
|
|
||||||
x1: self.rect.max.x.into(),
|
|
||||||
y1: self.rect.max.y.into(),
|
|
||||||
});
|
|
||||||
if self.sense.focusable {
|
|
||||||
builder.add_action(accesskit::Action::Focus);
|
|
||||||
}
|
|
||||||
if self.sense.click && builder.default_action_verb().is_none() {
|
|
||||||
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
fn fill_accesskit_node_from_widget_info(
|
|
||||||
&self,
|
|
||||||
builder: &mut accesskit::NodeBuilder,
|
|
||||||
info: crate::WidgetInfo,
|
|
||||||
) {
|
|
||||||
use crate::WidgetType;
|
|
||||||
use accesskit::{CheckedState, Role};
|
|
||||||
|
|
||||||
self.fill_accesskit_node_common(builder);
|
|
||||||
builder.set_role(match info.typ {
|
|
||||||
WidgetType::Label => Role::StaticText,
|
|
||||||
WidgetType::Link => Role::Link,
|
|
||||||
WidgetType::TextEdit => Role::TextField,
|
|
||||||
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
|
|
||||||
Role::Button
|
|
||||||
}
|
|
||||||
WidgetType::Checkbox => Role::CheckBox,
|
|
||||||
WidgetType::RadioButton => Role::RadioButton,
|
|
||||||
WidgetType::SelectableLabel => Role::ToggleButton,
|
|
||||||
WidgetType::ComboBox => Role::PopupButton,
|
|
||||||
WidgetType::Slider => Role::Slider,
|
|
||||||
WidgetType::DragValue => Role::SpinButton,
|
|
||||||
WidgetType::ColorButton => Role::ColorWell,
|
|
||||||
WidgetType::Other => Role::Unknown,
|
|
||||||
});
|
|
||||||
if let Some(label) = info.label {
|
|
||||||
builder.set_name(label);
|
|
||||||
}
|
|
||||||
if let Some(value) = info.current_text_value {
|
|
||||||
builder.set_value(value);
|
|
||||||
}
|
|
||||||
if let Some(value) = info.value {
|
|
||||||
builder.set_numeric_value(value);
|
|
||||||
}
|
|
||||||
if let Some(selected) = info.selected {
|
|
||||||
builder.set_checked_state(if selected {
|
|
||||||
CheckedState::True
|
|
||||||
} else {
|
|
||||||
CheckedState::False
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Associate a label with a control for accessibility.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// # let mut text = "Arthur".to_string();
|
|
||||||
/// ui.horizontal(|ui| {
|
|
||||||
/// let label = ui.label("Your name: ");
|
|
||||||
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
|
|
||||||
/// });
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
pub fn labelled_by(self, id: Id) -> Self {
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
|
||||||
builder.push_labelled_by(id.accesskit_id());
|
|
||||||
});
|
|
||||||
#[cfg(not(feature = "accesskit"))]
|
|
||||||
{
|
|
||||||
let _ = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -713,7 +570,6 @@ 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],
|
||||||
|
@ -745,13 +601,6 @@ 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:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#![allow(clippy::if_same_then_else)]
|
#![allow(clippy::if_same_then_else)]
|
||||||
|
|
||||||
use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
||||||
use epaint::{Rounding, Shadow, Stroke};
|
use epaint::{Rounding, Shadow, Stroke};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
@ -174,9 +174,6 @@ 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`.
|
||||||
|
@ -219,7 +216,6 @@ 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;
|
||||||
|
@ -268,11 +264,8 @@ 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`].
|
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
|
||||||
pub slider_width: f32,
|
pub slider_width: f32, // TODO(emilk): rename big_interact_size ?
|
||||||
|
|
||||||
/// 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,
|
||||||
|
@ -300,12 +293,8 @@ 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,
|
||||||
}
|
}
|
||||||
|
@ -371,10 +360,6 @@ impl Margin {
|
||||||
pub fn right_bottom(&self) -> Vec2 {
|
pub fn right_bottom(&self) -> Vec2 {
|
||||||
vec2(self.right, self.bottom)
|
vec2(self.right, self.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_same(&self) -> bool {
|
|
||||||
self.left == self.right && self.left == self.top && self.left == self.bottom
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<f32> for Margin {
|
impl From<f32> for Margin {
|
||||||
|
@ -479,20 +464,12 @@ pub struct Visuals {
|
||||||
|
|
||||||
pub window_rounding: Rounding,
|
pub window_rounding: Rounding,
|
||||||
pub window_shadow: Shadow,
|
pub window_shadow: Shadow,
|
||||||
pub window_fill: Color32,
|
|
||||||
pub window_stroke: Stroke,
|
|
||||||
|
|
||||||
pub menu_rounding: Rounding,
|
|
||||||
|
|
||||||
/// Panel background color
|
|
||||||
pub panel_fill: Color32,
|
|
||||||
|
|
||||||
pub popup_shadow: Shadow,
|
pub popup_shadow: Shadow,
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
|
@ -504,18 +481,6 @@ 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
|
|
||||||
/// (have alternating rows differently colored).
|
|
||||||
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 {
|
||||||
|
@ -530,7 +495,7 @@ impl Visuals {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weak_text_color(&self) -> Color32 {
|
pub fn weak_text_color(&self) -> Color32 {
|
||||||
self.gray_out(self.text_color())
|
crate::color::tint_color_towards(self.text_color(), self.window_fill())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
@ -541,25 +506,12 @@ impl Visuals {
|
||||||
/// Window background color.
|
/// Window background color.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn window_fill(&self) -> Color32 {
|
pub fn window_fill(&self) -> Color32 {
|
||||||
self.window_fill
|
self.widgets.noninteractive.bg_fill
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn window_stroke(&self) -> Stroke {
|
pub fn window_stroke(&self) -> Stroke {
|
||||||
self.window_stroke
|
self.widgets.noninteractive.bg_stroke
|
||||||
}
|
|
||||||
|
|
||||||
/// When fading out things, we fade the colors towards this.
|
|
||||||
// TODO(emilk): replace with an alpha
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn fade_out_to_color(&self) -> Color32 {
|
|
||||||
self.widgets.noninteractive.weak_bg_fill
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returned a "grayed out" version of the given color.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn gray_out(&self, color: Color32) -> Color32 {
|
|
||||||
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,9 +538,7 @@ 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, or when it is highlighted.
|
/// The style of an interactive widget while you hover it.
|
||||||
///
|
|
||||||
/// 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.
|
||||||
|
@ -604,7 +554,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() || response.highlighted() {
|
} else if response.hovered() {
|
||||||
&self.hovered
|
&self.hovered
|
||||||
} else {
|
} else {
|
||||||
&self.inactive
|
&self.inactive
|
||||||
|
@ -616,17 +566,9 @@ 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 widgets that must have a background fill,
|
/// Background color of widget.
|
||||||
/// 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`.
|
||||||
|
@ -650,7 +592,7 @@ impl WidgetVisuals {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for help debug egui by adding extra visualization
|
/// Options for help debug egui by adding extra visualization
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct DebugOptions {
|
pub struct DebugOptions {
|
||||||
/// However over widgets to see their rectangles
|
/// However over widgets to see their rectangles
|
||||||
|
@ -693,7 +635,6 @@ 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(),
|
||||||
|
@ -710,12 +651,11 @@ impl Default for Spacing {
|
||||||
Self {
|
Self {
|
||||||
item_spacing: vec2(8.0, 3.0),
|
item_spacing: vec2(8.0, 3.0),
|
||||||
window_margin: Margin::same(6.0),
|
window_margin: Margin::same(6.0),
|
||||||
menu_margin: Margin::same(6.0),
|
menu_margin: Margin::same(1.0),
|
||||||
button_padding: vec2(4.0, 1.0),
|
button_padding: vec2(4.0, 1.0),
|
||||||
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,
|
||||||
|
@ -723,7 +663,6 @@ 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,
|
||||||
|
@ -750,21 +689,13 @@ 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_additive_luminance(5), // visible, but barely so
|
faint_bg_color: Color32::from_gray(35),
|
||||||
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
|
||||||
|
|
||||||
window_rounding: Rounding::same(6.0),
|
window_rounding: Rounding::same(6.0),
|
||||||
window_shadow: Shadow::big_dark(),
|
window_shadow: Shadow::big_dark(),
|
||||||
window_fill: Color32::from_gray(27),
|
|
||||||
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
|
||||||
|
|
||||||
menu_rounding: Rounding::same(6.0),
|
|
||||||
|
|
||||||
panel_fill: Color32::from_gray(27),
|
|
||||||
|
|
||||||
popup_shadow: Shadow::small_dark(),
|
popup_shadow: Shadow::small_dark(),
|
||||||
resize_corner_size: 12.0,
|
resize_corner_size: 12.0,
|
||||||
text_cursor_width: 2.0,
|
text_cursor_width: 2.0,
|
||||||
|
@ -772,11 +703,6 @@ 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,
|
|
||||||
|
|
||||||
slider_trailing_fill: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,18 +713,12 @@ 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_additive_luminance(5), // visible, but barely so
|
faint_bg_color: Color32::from_gray(242),
|
||||||
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
|
||||||
|
|
||||||
window_shadow: Shadow::big_light(),
|
window_shadow: Shadow::big_light(),
|
||||||
window_fill: Color32::from_gray(248),
|
|
||||||
window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
|
|
||||||
|
|
||||||
panel_fill: Color32::from_gray(248),
|
|
||||||
|
|
||||||
popup_shadow: Shadow::small_light(),
|
popup_shadow: Shadow::small_light(),
|
||||||
..Self::dark()
|
..Self::dark()
|
||||||
}
|
}
|
||||||
|
@ -837,23 +757,20 @@ 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), // window background
|
||||||
bg_fill: Color32::from_gray(27),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines
|
||||||
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
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2.0),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
weak_bg_fill: Color32::from_gray(60), // button background
|
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)),
|
||||||
|
@ -861,7 +778,6 @@ 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),
|
||||||
|
@ -869,7 +785,6 @@ 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)),
|
||||||
|
@ -882,23 +797,20 @@ 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), // window background - should be distinct from TextEdit background
|
||||||
bg_fill: Color32::from_gray(248),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines
|
||||||
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
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2.0),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
weak_bg_fill: Color32::from_gray(230), // button background
|
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),
|
||||||
|
@ -906,7 +818,6 @@ 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),
|
||||||
|
@ -914,7 +825,6 @@ 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),
|
||||||
|
@ -941,7 +851,6 @@ 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,
|
||||||
|
@ -983,19 +892,6 @@ 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)
|
||||||
|
@ -1044,7 +940,6 @@ 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,
|
||||||
|
@ -1053,15 +948,70 @@ 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;
|
||||||
|
|
||||||
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
|
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
|
||||||
|
|
||||||
margin_ui(ui, "Window margin:", window_margin);
|
let margin_range = 0.0..=20.0;
|
||||||
margin_ui(ui, "Menu margin:", menu_margin);
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut window_margin.left)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("left: "),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut window_margin.right)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("right: "),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.label("Window margins x");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut window_margin.top)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("top: "),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut window_margin.bottom)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("bottom: "),
|
||||||
|
);
|
||||||
|
ui.label("Window margins y");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut menu_margin.left)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("left: "),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut menu_margin.right)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("right: "),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.label("Menu margins x");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut menu_margin.top)
|
||||||
|
.clamp_range(margin_range.clone())
|
||||||
|
.prefix("top: "),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(&mut menu_margin.bottom)
|
||||||
|
.clamp_range(margin_range)
|
||||||
|
.prefix("bottom: "),
|
||||||
|
);
|
||||||
|
ui.label("Menu margins y");
|
||||||
|
});
|
||||||
|
|
||||||
ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding"));
|
ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding"));
|
||||||
ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size"))
|
ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size"))
|
||||||
|
@ -1074,10 +1024,6 @@ 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");
|
||||||
|
@ -1086,10 +1032,6 @@ 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");
|
||||||
|
@ -1137,55 +1079,6 @@ impl Spacing {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) {
|
|
||||||
let margin_range = 0.0..=20.0;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label(text);
|
|
||||||
|
|
||||||
let mut same = margin.is_same();
|
|
||||||
ui.checkbox(&mut same, "Same");
|
|
||||||
|
|
||||||
if same {
|
|
||||||
let mut value = margin.left;
|
|
||||||
ui.add(DragValue::new(&mut value).clamp_range(margin_range.clone()));
|
|
||||||
*margin = Margin::same(value);
|
|
||||||
} else {
|
|
||||||
if margin.is_same() {
|
|
||||||
// HACK: prevent collapse:
|
|
||||||
margin.right = margin.left + 1.0;
|
|
||||||
margin.bottom = margin.left + 2.0;
|
|
||||||
margin.top = margin.left + 3.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
DragValue::new(&mut margin.left)
|
|
||||||
.clamp_range(margin_range.clone())
|
|
||||||
.prefix("L: "),
|
|
||||||
)
|
|
||||||
.on_hover_text("Left margin");
|
|
||||||
ui.add(
|
|
||||||
DragValue::new(&mut margin.right)
|
|
||||||
.clamp_range(margin_range.clone())
|
|
||||||
.prefix("R: "),
|
|
||||||
)
|
|
||||||
.on_hover_text("Right margin");
|
|
||||||
ui.add(
|
|
||||||
DragValue::new(&mut margin.top)
|
|
||||||
.clamp_range(margin_range.clone())
|
|
||||||
.prefix("T: "),
|
|
||||||
)
|
|
||||||
.on_hover_text("Top margin");
|
|
||||||
ui.add(
|
|
||||||
DragValue::new(&mut margin.bottom)
|
|
||||||
.clamp_range(margin_range)
|
|
||||||
.prefix("B: "),
|
|
||||||
)
|
|
||||||
.on_hover_text("Bottom margin");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Interaction {
|
impl Interaction {
|
||||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||||
let Self {
|
let Self {
|
||||||
|
@ -1255,17 +1148,13 @@ 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 {
|
||||||
weak_bg_fill,
|
bg_fill,
|
||||||
bg_fill: mandatory_bg_fill,
|
|
||||||
bg_stroke,
|
bg_stroke,
|
||||||
rounding,
|
rounding,
|
||||||
fg_stroke,
|
fg_stroke,
|
||||||
expansion,
|
expansion,
|
||||||
} = self;
|
} = self;
|
||||||
ui_color(ui, weak_bg_fill, "optional background fill")
|
ui_color(ui, bg_fill, "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);
|
||||||
|
@ -1321,35 +1210,20 @@ impl Visuals {
|
||||||
code_bg_color,
|
code_bg_color,
|
||||||
warn_fg_color,
|
warn_fg_color,
|
||||||
error_fg_color,
|
error_fg_color,
|
||||||
|
|
||||||
window_rounding,
|
window_rounding,
|
||||||
window_shadow,
|
window_shadow,
|
||||||
window_fill,
|
|
||||||
window_stroke,
|
|
||||||
|
|
||||||
menu_rounding,
|
|
||||||
|
|
||||||
panel_fill,
|
|
||||||
|
|
||||||
popup_shadow,
|
popup_shadow,
|
||||||
|
|
||||||
resize_corner_size,
|
resize_corner_size,
|
||||||
text_cursor_width,
|
text_cursor_width,
|
||||||
text_cursor_preview,
|
text_cursor_preview,
|
||||||
clip_rect_margin,
|
clip_rect_margin,
|
||||||
button_frame,
|
button_frame,
|
||||||
collapsing_header_frame,
|
collapsing_header_frame,
|
||||||
indent_has_left_vline,
|
|
||||||
|
|
||||||
striped,
|
|
||||||
|
|
||||||
slider_trailing_fill,
|
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.collapsing("Background Colors", |ui| {
|
ui.collapsing("Background Colors", |ui| {
|
||||||
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
|
||||||
ui_color(ui, window_fill, "Windows");
|
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Windows");
|
||||||
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(
|
||||||
"Used for faint accentuation of interactive things, like striped grids.",
|
"Used for faint accentuation of interactive things, like striped grids.",
|
||||||
);
|
);
|
||||||
|
@ -1358,15 +1232,14 @@ impl Visuals {
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.collapsing("Window", |ui| {
|
ui.collapsing("Window", |ui| {
|
||||||
ui_color(ui, window_fill, "Fill");
|
// Common shortcuts
|
||||||
stroke_ui(ui, window_stroke, "Outline");
|
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill");
|
||||||
rounding_ui(ui, window_rounding);
|
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
|
||||||
shadow_ui(ui, window_shadow, "Shadow");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.collapsing("Menus and popups", |ui| {
|
rounding_ui(ui, window_rounding);
|
||||||
rounding_ui(ui, menu_rounding);
|
|
||||||
shadow_ui(ui, popup_shadow, "Shadow");
|
shadow_ui(ui, window_shadow, "Shadow");
|
||||||
|
shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)");
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.collapsing("Widgets", |ui| widgets.ui(ui));
|
ui.collapsing("Widgets", |ui| widgets.ui(ui));
|
||||||
|
@ -1399,14 +1272,6 @@ 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(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;
|
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||||
util::IdTypeMap, widgets::*, *,
|
widgets::*, *,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -97,17 +97,19 @@ impl Ui {
|
||||||
id_source: impl Hash,
|
id_source: impl Hash,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
crate::egui_assert!(!max_rect.any_nan());
|
crate::egui_assert!(!max_rect.any_nan());
|
||||||
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
|
let child_id = self.id.with(id_source);
|
||||||
|
let child_next_auto_id_source = child_id.with(self.next_auto_id_source).value();
|
||||||
|
|
||||||
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
|
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
|
||||||
let menu_state = self.menu_state();
|
|
||||||
Ui {
|
Ui {
|
||||||
id: self.id.with(id_source),
|
id: child_id,
|
||||||
next_auto_id_source,
|
next_auto_id_source: child_next_auto_id_source,
|
||||||
painter: self.painter.clone(),
|
painter: self.painter.clone(),
|
||||||
style: self.style.clone(),
|
style: self.style.clone(),
|
||||||
placer: Placer::new(max_rect, layout),
|
placer: Placer::new(max_rect, layout),
|
||||||
enabled: self.enabled,
|
enabled: self.enabled,
|
||||||
menu_state,
|
menu_state: self.menu_state.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,7 +241,7 @@ impl Ui {
|
||||||
self.enabled &= enabled;
|
self.enabled &= enabled;
|
||||||
if !self.enabled && self.is_visible() {
|
if !self.enabled && self.is_visible() {
|
||||||
self.painter
|
self.painter
|
||||||
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
|
.set_fade_to_color(Some(self.visuals().window_fill()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,9 +316,84 @@ 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(|f| f.row_height(&style.resolve(self.style())))
|
self.fonts().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.
|
||||||
|
@ -338,87 +415,6 @@ 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
|
||||||
|
@ -967,8 +963,7 @@ 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()
|
self.ctx().frame_state().scroll_target[d] = Some((range, align));
|
||||||
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -997,8 +992,7 @@ 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()
|
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
|
||||||
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1030,8 +1024,7 @@ impl Ui {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scroll_with_delta(&self, delta: Vec2) {
|
pub fn scroll_with_delta(&self, delta: Vec2) {
|
||||||
self.ctx()
|
self.ctx().frame_state().scroll_delta += delta;
|
||||||
.frame_state_mut(|state| state.scroll_delta += delta);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1572,7 +1565,7 @@ impl Ui {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// See also [`crate::Image`] and [`crate::ImageButton`].
|
/// Se 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)
|
||||||
|
@ -1789,32 +1782,25 @@ 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 left_vline || end_with_horizontal_line {
|
|
||||||
if end_with_horizontal_line {
|
if end_with_horizontal_line {
|
||||||
child_ui.add_space(4.0);
|
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);
|
||||||
|
|
||||||
if left_vline {
|
|
||||||
// draw a faint line on the left to mark the indented section
|
|
||||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||||
}
|
|
||||||
|
|
||||||
if end_with_horizontal_line {
|
if end_with_horizontal_line {
|
||||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
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);
|
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||||
self.painter
|
self.painter
|
||||||
.line_segment([left_bottom, right_bottom], stroke);
|
.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());
|
||||||
InnerResponse::new(ret, response)
|
InnerResponse::new(ret, response)
|
||||||
|
@ -2023,14 +2009,12 @@ impl Ui {
|
||||||
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
|
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated = "Use ui.vertical_centered or ui.centered_and_justified"]
|
/// This will make the next added widget centered in the available space.
|
||||||
pub fn centered<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> InnerResponse<R> {
|
pub fn centered<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> InnerResponse<R> {
|
||||||
self.vertical_centered(add_contents)
|
self.with_layout_dyn(Layout::centered(Direction::TopDown), Box::new(add_contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This will make the next added widget centered and justified in the available space.
|
/// This will make the next added widget centered and justified in the available space.
|
||||||
///
|
|
||||||
/// Only one widget may be added to the inner `Ui`!
|
|
||||||
pub fn centered_and_justified<R>(
|
pub fn centered_and_justified<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
add_contents: impl FnOnce(&mut Self) -> R,
|
add_contents: impl FnOnce(&mut Self) -> R,
|
||||||
|
@ -2136,10 +2120,6 @@ impl Ui {
|
||||||
self.menu_state = None;
|
self.menu_state = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn menu_state(&self) -> Option<Arc<RwLock<MenuState>>> {
|
|
||||||
self.menu_state.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
|
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
|
||||||
self.menu_state = menu_state;
|
self.menu_state = menu_state;
|
||||||
}
|
}
|
||||||
|
@ -2173,43 +2153,6 @@ 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(&self) -> bool {
|
pub fn is_empty(&mut self) -> bool {
|
||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&mut 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(&self) -> usize {
|
pub fn count_serialized(&mut 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>(&self) -> usize {
|
pub fn count<T: 'static>(&mut self) -> usize {
|
||||||
let key = TypeId::of::<T>();
|
let key = TypeId::of::<T>();
|
||||||
self.0
|
self.0
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -293,12 +293,12 @@ impl RichText {
|
||||||
let underline = if underline {
|
let underline = if underline {
|
||||||
crate::Stroke::new(1.0, line_color)
|
crate::Stroke::new(1.0, line_color)
|
||||||
} else {
|
} else {
|
||||||
crate::Stroke::NONE
|
crate::Stroke::none()
|
||||||
};
|
};
|
||||||
let strikethrough = if strikethrough {
|
let strikethrough = if strikethrough {
|
||||||
crate::Stroke::new(1.0, line_color)
|
crate::Stroke::new(1.0, line_color)
|
||||||
} else {
|
} else {
|
||||||
crate::Stroke::NONE
|
crate::Stroke::none()
|
||||||
};
|
};
|
||||||
|
|
||||||
let valign = if raised {
|
let valign = if raised {
|
||||||
|
@ -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(|f| f.layout_job(text_job.job)),
|
galley: ui.fonts().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(|f| f.layout_job(job)),
|
galley: ui.fonts().layout_job(job),
|
||||||
galley_has_color: true,
|
galley_has_color: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +45,6 @@ impl Button {
|
||||||
small: false,
|
small: false,
|
||||||
frame: None,
|
frame: None,
|
||||||
min_size: Vec2::ZERO,
|
min_size: Vec2::ZERO,
|
||||||
rounding: None,
|
|
||||||
image: None,
|
image: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,12 +117,6 @@ 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`).
|
||||||
|
@ -148,7 +140,6 @@ impl Widget for Button {
|
||||||
small,
|
small,
|
||||||
frame,
|
frame,
|
||||||
min_size,
|
min_size,
|
||||||
rounding,
|
|
||||||
image,
|
image,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -180,10 +171,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);
|
||||||
|
@ -193,11 +184,14 @@ 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.weak_bg_fill);
|
let fill = fill.unwrap_or(visuals.bg_fill);
|
||||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
||||||
let rounding = rounding.unwrap_or(visuals.rounding);
|
ui.painter().rect(
|
||||||
ui.painter()
|
rect.expand(visuals.expansion),
|
||||||
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
visuals.rounding,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_pos = if let Some(image) = image {
|
let text_pos = if let Some(image) = image {
|
||||||
|
@ -227,10 +221,7 @@ 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(
|
pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)),
|
||||||
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);
|
||||||
|
@ -269,10 +260,6 @@ 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> {
|
||||||
|
@ -544,7 +531,7 @@ impl Widget for ImageButton {
|
||||||
(
|
(
|
||||||
expansion,
|
expansion,
|
||||||
visuals.rounding,
|
visuals.rounding,
|
||||||
visuals.weak_bg_fill,
|
visuals.bg_fill,
|
||||||
visuals.bg_stroke,
|
visuals.bg_stroke,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use crate::util::fixed_cache::FixedCache;
|
use crate::util::fixed_cache::FixedCache;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use epaint::{ecolor::*, *};
|
use epaint::{color::*, *};
|
||||||
|
|
||||||
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
|
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
|
||||||
if color.into().intensity() < 0.5 {
|
if color.into().intensity() < 0.5 {
|
||||||
|
@ -211,7 +211,7 @@ fn color_slider_2d(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What options to show for alpha
|
/// What options to show for alpha
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum Alpha {
|
pub enum Alpha {
|
||||||
// Set alpha to 1.0, and show no option for it.
|
// Set alpha to 1.0, and show no option for it.
|
||||||
Opaque,
|
Opaque,
|
||||||
|
@ -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_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
|
ui.output().copied_text = format!("{}, {}, {}", r, g, b);
|
||||||
} else {
|
} else {
|
||||||
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
|
ui.output().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(|mem| mem.is_popup_open(popup_id));
|
let open = ui.memory().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_mut(|mem| mem.toggle_popup(popup_id));
|
ui.memory().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(|mem| mem.is_popup_open(popup_id)) {
|
if ui.memory().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(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
|
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
|
||||||
{
|
{
|
||||||
ui.memory_mut(|mem| mem.close_popup());
|
ui.memory().close_popup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,7 +425,7 @@ pub fn color_edit_button_rgb(ui: &mut Ui, rgb: &mut [f32; 3]) -> Response {
|
||||||
// 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 color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
|
fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
|
||||||
let rgba = rgba.into();
|
let rgba = rgba.into();
|
||||||
use_color_cache(ctx, |cc| cc.get(&rgba).copied()).unwrap_or_else(|| Hsva::from(rgba))
|
use_color_cache(ctx, |cc| cc.get(&rgba).cloned()).unwrap_or_else(|| Hsva::from(rgba))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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:
|
||||||
|
@ -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 {
|
||||||
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
|
f(ctx.data().get_temp_mut_or_default(Id::null()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -368,37 +368,28 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
custom_parser,
|
custom_parser,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let shift = ui.input(|i| i.modifiers.shift_only());
|
let shift = ui.input().modifiers.shift_only();
|
||||||
// The widget has the same ID whether it's in edit or button mode.
|
let is_slow_speed = shift && ui.memory().is_being_dragged(ui.next_auto_id());
|
||||||
let id = ui.next_auto_id();
|
|
||||||
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
|
|
||||||
|
|
||||||
// The following ensures that when a `DragValue` receives focus,
|
let kb_edit_id = ui.next_auto_id();
|
||||||
|
// The following call 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.
|
||||||
let is_kb_editing = ui.memory_mut(|mem| {
|
ui.memory().interested_in_focus(kb_edit_id);
|
||||||
mem.interested_in_focus(id);
|
let is_kb_editing = ui.memory().has_focus(kb_edit_id);
|
||||||
let is_kb_editing = mem.has_focus(id);
|
|
||||||
if mem.gained_focus(id) {
|
|
||||||
mem.drag_value.edit_string = None;
|
|
||||||
}
|
|
||||||
is_kb_editing
|
|
||||||
});
|
|
||||||
|
|
||||||
let old_value = get(&mut get_set_value);
|
let old_value = get(&mut get_set_value);
|
||||||
let mut value = old_value;
|
let mut value = old_value;
|
||||||
let aim_rad = ui.input(|i| i.aim_radius() as f64);
|
let aim_rad = ui.input().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 = ui.input_mut(|input| {
|
|
||||||
let mut change = 0.0;
|
|
||||||
|
|
||||||
if is_kb_editing {
|
if is_kb_editing {
|
||||||
|
let mut input = ui.input_mut();
|
||||||
// This deliberately doesn't listen for left and right arrow keys,
|
// This deliberately doesn't listen for left and right arrow keys,
|
||||||
// because when editing, these are used to move the caret.
|
// because when editing, these are used to move the caret.
|
||||||
// This behavior is consistent with other editable spinner/stepper
|
// This behavior is consistent with other editable spinner/stepper
|
||||||
|
@ -408,41 +399,19 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
// assume this behavior, so having a separate mode for incrementing
|
// assume this behavior, so having a separate mode for incrementing
|
||||||
// and decrementing, that supports all arrow keys, would be
|
// and decrementing, that supports all arrow keys, would be
|
||||||
// problematic.
|
// problematic.
|
||||||
change += input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
|
let change = input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
|
||||||
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
use accesskit::Action;
|
|
||||||
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
|
|
||||||
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
|
||||||
}
|
|
||||||
|
|
||||||
change
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
use accesskit::{Action, ActionData};
|
|
||||||
ui.input(|input| {
|
|
||||||
for request in input.accesskit_action_requests(id, Action::SetValue) {
|
|
||||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
|
||||||
value = new_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if change != 0.0 {
|
if change != 0.0 {
|
||||||
value += speed * change;
|
value += speed * change;
|
||||||
value = emath::round_to_decimals(value, auto_decimals);
|
value = emath::round_to_decimals(value, auto_decimals);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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_mut(|mem| mem.drag_value.edit_string = None);
|
ui.memory().drag_value.edit_string = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let value_text = match custom_formatter {
|
let value_text = match custom_formatter {
|
||||||
|
@ -456,43 +425,34 @@ 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
|
|
||||||
#[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_mut(|mem| mem.drag_value.edit_string.take())
|
.memory()
|
||||||
.unwrap_or_else(|| value_text.clone());
|
.drag_value
|
||||||
|
.edit_string
|
||||||
|
.take()
|
||||||
|
.unwrap_or(value_text);
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
TextEdit::singleline(&mut value_text)
|
TextEdit::singleline(&mut value_text)
|
||||||
.clip_text(false)
|
.id(kb_edit_id)
|
||||||
.horizontal_align(ui.layout().horizontal_align())
|
.desired_width(button_width)
|
||||||
.vertical_align(ui.layout().vertical_align())
|
.font(TextStyle::Monospace),
|
||||||
.margin(ui.spacing().button_padding)
|
|
||||||
.min_size(ui.spacing().interact_size)
|
|
||||||
.id(id)
|
|
||||||
.desired_width(ui.spacing().interact_size.x)
|
|
||||||
.font(text_style),
|
|
||||||
);
|
);
|
||||||
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
|
|
||||||
// See https://github.com/emilk/egui/issues/2687
|
|
||||||
if response.lost_focus() {
|
|
||||||
let parsed_value = match custom_parser {
|
let parsed_value = match custom_parser {
|
||||||
Some(parser) => parser(&value_text),
|
Some(parser) => parser(&value_text),
|
||||||
None => value_text.parse().ok(),
|
None => value_text.parse().ok(),
|
||||||
};
|
};
|
||||||
if let Some(parsed_value) = parsed_value {
|
if let Some(parsed_value) = parsed_value {
|
||||||
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
|
let parsed_value = clamp_to_range(parsed_value, clamp_range);
|
||||||
set(&mut get_set_value, parsed_value);
|
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 {
|
||||||
|
ui.memory().drag_value.edit_string = None;
|
||||||
let button = Button::new(
|
let button = Button::new(
|
||||||
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
|
RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
|
||||||
.text_style(text_style),
|
|
||||||
)
|
)
|
||||||
.wrap(false)
|
.wrap(false)
|
||||||
.sense(Sense::click_and_drag())
|
.sense(Sense::click_and_drag())
|
||||||
|
@ -511,18 +471,9 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
ui.memory_mut(|mem| {
|
ui.memory().request_focus(kb_edit_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.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
ui.output().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
|
||||||
|
@ -532,7 +483,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 = ui.memory_mut(|mem| std::mem::take(&mut mem.drag_value));
|
let mut drag_state = std::mem::take(&mut ui.memory().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))
|
||||||
|
@ -548,12 +499,12 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
);
|
);
|
||||||
let rounded_new_value =
|
let rounded_new_value =
|
||||||
emath::round_to_decimals(rounded_new_value, auto_decimals);
|
emath::round_to_decimals(rounded_new_value, auto_decimals);
|
||||||
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range.clone());
|
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range);
|
||||||
set(&mut get_set_value, rounded_new_value);
|
set(&mut get_set_value, rounded_new_value);
|
||||||
|
|
||||||
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_mut(|mem| mem.drag_value = drag_state);
|
ui.memory().drag_value = drag_state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,54 +514,6 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
response.changed = get(&mut get_set_value) != old_value;
|
response.changed = get(&mut get_set_value) != old_value;
|
||||||
|
|
||||||
response.widget_info(|| WidgetInfo::drag_value(value));
|
response.widget_info(|| WidgetInfo::drag_value(value));
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
|
||||||
use accesskit::Action;
|
|
||||||
// If either end of the range is unbounded, it's better
|
|
||||||
// to leave the corresponding AccessKit field set to None,
|
|
||||||
// to allow for platform-specific default behavior.
|
|
||||||
if clamp_range.start().is_finite() {
|
|
||||||
builder.set_min_numeric_value(*clamp_range.start());
|
|
||||||
}
|
|
||||||
if clamp_range.end().is_finite() {
|
|
||||||
builder.set_max_numeric_value(*clamp_range.end());
|
|
||||||
}
|
|
||||||
builder.set_numeric_value_step(speed);
|
|
||||||
builder.add_action(Action::SetValue);
|
|
||||||
if value < *clamp_range.end() {
|
|
||||||
builder.add_action(Action::Increment);
|
|
||||||
}
|
|
||||||
if value > *clamp_range.start() {
|
|
||||||
builder.add_action(Action::Decrement);
|
|
||||||
}
|
|
||||||
// The name field is set to the current value by the button,
|
|
||||||
// but we don't want it set that way on this widget type.
|
|
||||||
builder.clear_name();
|
|
||||||
// Always expose the value as a string. This makes the widget
|
|
||||||
// more stable to accessibility users as it switches
|
|
||||||
// between edit and button modes. This is particularly important
|
|
||||||
// for VoiceOver on macOS; if the value is not exposed as a string
|
|
||||||
// when the widget is in button mode, then VoiceOver speaks
|
|
||||||
// the value (or a percentage if the widget has a clamp range)
|
|
||||||
// when the widget loses focus, overriding the announcement
|
|
||||||
// of the newly focused widget. This is certainly a VoiceOver bug,
|
|
||||||
// but it's good to make our software work as well as possible
|
|
||||||
// with existing assistive technology. However, if the widget
|
|
||||||
// has a prefix and/or suffix, expose those when in button mode,
|
|
||||||
// just as they're exposed on the screen. This triggers the
|
|
||||||
// VoiceOver bug just described, but exposing all information
|
|
||||||
// is more important, and at least we can avoid the bug
|
|
||||||
// for instances of the widget with no prefix or suffix.
|
|
||||||
//
|
|
||||||
// The value is exposed as a string by the text edit widget
|
|
||||||
// when in edit mode.
|
|
||||||
if !is_kb_editing {
|
|
||||||
let value_text = format!("{}{}{}", prefix, value_text, suffix);
|
|
||||||
builder.set_value(value_text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
response
|
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().set_cursor_icon(CursorIcon::PointingHand);
|
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
|
@ -48,7 +48,7 @@ impl Widget for Link {
|
||||||
let underline = if response.hovered() || response.has_focus() {
|
let underline = if response.hovered() || response.has_focus() {
|
||||||
Stroke::new(visuals.fg_stroke.width, color)
|
Stroke::new(visuals.fg_stroke.width, color)
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::none()
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter().add(epaint::TextShape {
|
ui.painter().add(epaint::TextShape {
|
||||||
|
@ -110,21 +110,17 @@ 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(|i| i.modifiers);
|
let modifiers = ui.ctx().input().modifiers;
|
||||||
ui.ctx().output_mut(|o| {
|
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
||||||
o.open_url = Some(crate::output::OpenUrl {
|
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
new_tab: modifiers.any(),
|
new_tab: modifiers.any(),
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if response.middle_clicked() {
|
if response.middle_clicked() {
|
||||||
ui.ctx().output_mut(|o| {
|
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
||||||
o.open_url = Some(crate::output::OpenUrl {
|
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
new_tab: true,
|
new_tab: true,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
response.on_hover_text(url)
|
response.on_hover_text(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{widget_text::WidgetTextGalley, *};
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
text: WidgetText,
|
text: WidgetText,
|
||||||
wrap: Option<bool>,
|
wrap: Option<bool>,
|
||||||
sense: Option<Sense>,
|
sense: Sense,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Label {
|
impl Label {
|
||||||
|
@ -24,7 +24,7 @@ impl Label {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
wrap: None,
|
wrap: None,
|
||||||
sense: None,
|
sense: Sense::focusable_noninteractive(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,13 +34,11 @@ impl Label {
|
||||||
|
|
||||||
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
|
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
|
||||||
///
|
///
|
||||||
/// By default [`Self::wrap`] will be `true` in vertical layouts
|
/// By default [`Self::wrap`] will be true in vertical layouts
|
||||||
/// and horizontal layouts with wrapping,
|
/// and horizontal layouts with wrapping,
|
||||||
/// and `false` on non-wrapping horizontal layouts.
|
/// and false on non-wrapping horizontal layouts.
|
||||||
///
|
///
|
||||||
/// Note that any `\n` in the text will always produce a new line.
|
/// Note that any `\n` in the text will always produce a new line.
|
||||||
///
|
|
||||||
/// You can also use [`crate::Style::wrap`].
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||||
self.wrap = Some(wrap);
|
self.wrap = Some(wrap);
|
||||||
|
@ -62,7 +60,7 @@ impl Label {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn sense(mut self, sense: Sense) -> Self {
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
self.sense = Some(sense);
|
self.sense = sense;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,17 +68,9 @@ impl Label {
|
||||||
impl Label {
|
impl Label {
|
||||||
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
||||||
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(|| {
|
|
||||||
// We only want to focus labels if the screen reader is on.
|
|
||||||
if ui.memory(|mem| mem.options.screen_reader) {
|
|
||||||
Sense::focusable_noninteractive()
|
|
||||||
} else {
|
|
||||||
Sense::hover()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let WidgetText::Galley(galley) = self.text {
|
if let WidgetText::Galley(galley) = self.text {
|
||||||
// If the user said "use this specific galley", then just use it:
|
// If the user said "use this specific galley", then just use it:
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
|
||||||
let pos = match galley.job.halign {
|
let pos = match galley.job.halign {
|
||||||
Align::LEFT => rect.left_top(),
|
Align::LEFT => rect.left_top(),
|
||||||
Align::Center => rect.center_top(),
|
Align::Center => rect.center_top(),
|
||||||
|
@ -120,7 +110,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 = ui.fonts(|f| text_job.into_galley(f));
|
let text_galley = text_job.into_galley(&ui.fonts());
|
||||||
|
|
||||||
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -131,10 +121,10 @@ impl Label {
|
||||||
let rect = text_galley.galley.rows[0]
|
let rect = text_galley.galley.rows[0]
|
||||||
.rect
|
.rect
|
||||||
.translate(vec2(pos.x, pos.y));
|
.translate(vec2(pos.x, pos.y));
|
||||||
let mut response = ui.allocate_rect(rect, sense);
|
let mut response = ui.allocate_rect(rect, self.sense);
|
||||||
for row in text_galley.galley.rows.iter().skip(1) {
|
for row in text_galley.galley.rows.iter().skip(1) {
|
||||||
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
||||||
response |= ui.allocate_rect(rect, sense);
|
response |= ui.allocate_rect(rect, self.sense);
|
||||||
}
|
}
|
||||||
(pos, text_galley, response)
|
(pos, text_galley, response)
|
||||||
} else {
|
} else {
|
||||||
|
@ -153,8 +143,8 @@ impl Label {
|
||||||
text_job.job.justify = ui.layout().horizontal_justify();
|
text_job.job.justify = ui.layout().horizontal_justify();
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_galley = ui.fonts(|f| text_job.into_galley(f));
|
let text_galley = text_job.into_galley(&ui.fonts());
|
||||||
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
|
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.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(),
|
||||||
Align::Center => rect.center_top(),
|
Align::Center => rect.center_top(),
|
||||||
|
@ -173,10 +163,10 @@ 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() || response.highlighted() {
|
let underline = if response.has_focus() {
|
||||||
Stroke::new(1.0, response_color)
|
Stroke::new(1.0, response_color)
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::none()
|
||||||
};
|
};
|
||||||
|
|
||||||
let override_text_color = if text_galley.galley_has_color {
|
let override_text_color = if text_galley.galley_has_color {
|
||||||
|
|
|
@ -185,11 +185,7 @@ impl RectElement for Bar {
|
||||||
|
|
||||||
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||||
let scale = transform.dvalue_dpos();
|
let scale = transform.dvalue_dpos();
|
||||||
let scale = match self.orientation {
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
Orientation::Horizontal => scale[0],
|
format!("\n{:.*}", y_decimals, self.value)
|
||||||
Orientation::Vertical => scale[1],
|
|
||||||
};
|
|
||||||
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
|
||||||
crate::plot::format_number(self.value, decimals)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,15 +269,9 @@ impl RectElement for BoxElem {
|
||||||
|
|
||||||
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||||
let scale = transform.dvalue_dpos();
|
let scale = transform.dvalue_dpos();
|
||||||
let scale = match self.orientation {
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
Orientation::Horizontal => scale[0],
|
|
||||||
Orientation::Vertical => scale[1],
|
|
||||||
};
|
|
||||||
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
|
|
||||||
.at_most(6)
|
|
||||||
.at_least(1);
|
|
||||||
format!(
|
format!(
|
||||||
"Max = {max:.decimals$}\
|
"\nMax = {max:.decimals$}\
|
||||||
\nQuartile 3 = {q3:.decimals$}\
|
\nQuartile 3 = {q3:.decimals$}\
|
||||||
\nMedian = {med:.decimals$}\
|
\nMedian = {med:.decimals$}\
|
||||||
\nQuartile 1 = {q1:.decimals$}\
|
\nQuartile 1 = {q1:.decimals$}\
|
||||||
|
|
|
@ -34,7 +34,6 @@ pub(super) struct PlotConfig<'a> {
|
||||||
pub(super) trait PlotItem {
|
pub(super) trait PlotItem {
|
||||||
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
||||||
|
|
||||||
/// For plot-items which are generated based on x values (plotting functions).
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
||||||
|
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
@ -606,7 +605,7 @@ impl PlotItem for Polygon {
|
||||||
|
|
||||||
let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha);
|
let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha);
|
||||||
|
|
||||||
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::NONE);
|
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none());
|
||||||
shapes.push(shape);
|
shapes.push(shape);
|
||||||
values_tf.push(*values_tf.first().unwrap());
|
values_tf.push(*values_tf.first().unwrap());
|
||||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
style.style_line(values_tf, *stroke, *highlight, shapes);
|
||||||
|
@ -856,11 +855,10 @@ impl PlotItem for Points {
|
||||||
|
|
||||||
let default_stroke = Stroke::new(stroke_size, *color);
|
let default_stroke = Stroke::new(stroke_size, *color);
|
||||||
let mut stem_stroke = default_stroke;
|
let mut stem_stroke = default_stroke;
|
||||||
let (fill, stroke) = if *filled {
|
let stroke = (!filled)
|
||||||
(*color, Stroke::NONE)
|
.then_some(default_stroke)
|
||||||
} else {
|
.unwrap_or_else(Stroke::none);
|
||||||
(Color32::TRANSPARENT, default_stroke)
|
let fill = filled.then(|| *color).unwrap_or_default();
|
||||||
};
|
|
||||||
|
|
||||||
if *highlight {
|
if *highlight {
|
||||||
radius *= 2f32.sqrt();
|
radius *= 2f32.sqrt();
|
||||||
|
@ -1648,7 +1646,6 @@ fn add_rulers_and_text(
|
||||||
let mut text = elem.name().to_owned(); // could be empty
|
let mut text = elem.name().to_owned(); // could be empty
|
||||||
|
|
||||||
if show_values {
|
if show_values {
|
||||||
text.push('\n');
|
|
||||||
text.push_str(&elem.default_values_format(plot.transform));
|
text.push_str(&elem.default_values_format(plot.transform));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1658,16 +1655,14 @@ 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();
|
||||||
plot.ui.fonts(|f| {
|
|
||||||
shapes.push(Shape::text(
|
shapes.push(Shape::text(
|
||||||
f,
|
&plot.ui.fonts(),
|
||||||
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
|
plot.transform.position_from_point(&corner_value) + 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(),
|
||||||
));
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
||||||
|
@ -1697,8 +1692,8 @@ pub(super) fn rulers_at_value(
|
||||||
|
|
||||||
let text = {
|
let text = {
|
||||||
let scale = plot.transform.dvalue_dpos();
|
let scale = plot.transform.dvalue_dpos();
|
||||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
|
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
|
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||||
if let Some(custom_label) = label_formatter {
|
if let Some(custom_label) = label_formatter {
|
||||||
custom_label(name, &value)
|
custom_label(name, &value)
|
||||||
} else if plot.show_x && plot.show_y {
|
} else if plot.show_x && plot.show_y {
|
||||||
|
@ -1716,16 +1711,15 @@ 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(
|
||||||
f,
|
&plot.ui.fonts(),
|
||||||
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>(
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::*;
|
||||||
use super::items::PlotItem;
|
use super::items::PlotItem;
|
||||||
|
|
||||||
/// Where to place the plot legend.
|
/// Where to place the plot legend.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum Corner {
|
pub enum Corner {
|
||||||
LeftTop,
|
LeftTop,
|
||||||
RightTop,
|
RightTop,
|
||||||
|
@ -89,7 +89,9 @@ impl LegendEntry {
|
||||||
|
|
||||||
let font_id = text_style.resolve(ui.style());
|
let font_id = text_style.resolve(ui.style());
|
||||||
|
|
||||||
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
|
let galley = ui
|
||||||
|
.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;
|
||||||
|
@ -237,7 +239,7 @@ impl Widget for &mut LegendWidget {
|
||||||
let background_frame = Frame {
|
let background_frame = Frame {
|
||||||
inner_margin: vec2(8.0, 4.0).into(),
|
inner_margin: vec2(8.0, 4.0).into(),
|
||||||
rounding: ui.style().visuals.window_rounding,
|
rounding: ui.style().visuals.window_rounding,
|
||||||
shadow: epaint::Shadow::NONE,
|
shadow: epaint::Shadow::default(),
|
||||||
fill: ui.style().visuals.extreme_bg_color,
|
fill: ui.style().visuals.extreme_bg_color,
|
||||||
stroke: ui.style().visuals.window_stroke(),
|
stroke: ui.style().visuals.window_stroke(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -7,8 +7,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use epaint::color::Hsva;
|
||||||
use epaint::util::FloatOrd;
|
use epaint::util::FloatOrd;
|
||||||
use epaint::Hsva;
|
|
||||||
|
|
||||||
use items::PlotItem;
|
use items::PlotItem;
|
||||||
use legend::LegendWidget;
|
use legend::LegendWidget;
|
||||||
|
@ -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_mut(|d| d.get_persisted(id))
|
ctx.data().get_persisted(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
ctx.data().insert_persisted(id, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,10 +291,7 @@ pub struct Plot {
|
||||||
legend_config: Option<Legend>,
|
legend_config: Option<Legend>,
|
||||||
show_background: bool,
|
show_background: bool,
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
|
|
||||||
grid_spacers: [GridSpacer; 2],
|
grid_spacers: [GridSpacer; 2],
|
||||||
sharp_grid_lines: bool,
|
|
||||||
clamp_grid: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plot {
|
impl Plot {
|
||||||
|
@ -333,10 +330,7 @@ impl Plot {
|
||||||
legend_config: None,
|
legend_config: None,
|
||||||
show_background: true,
|
show_background: true,
|
||||||
show_axes: [true; 2],
|
show_axes: [true; 2],
|
||||||
|
|
||||||
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
||||||
sharp_grid_lines: true,
|
|
||||||
clamp_grid: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -561,14 +555,6 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clamp the grid to only be visible at the range of data where we have values.
|
|
||||||
///
|
|
||||||
/// Default: `false`.
|
|
||||||
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
|
|
||||||
self.clamp_grid = clamp_grid;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expand bounds to include the given x value.
|
/// Expand bounds to include the given x value.
|
||||||
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
|
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
|
||||||
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
||||||
|
@ -631,13 +617,6 @@ impl Plot {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an
|
|
||||||
/// undesired effect when shifting the plot bounds. Enabled by default.
|
|
||||||
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
|
|
||||||
self.sharp_grid_lines = enabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the plot.
|
/// Resets the plot.
|
||||||
pub fn reset(mut self) -> Self {
|
pub fn reset(mut self) -> Self {
|
||||||
self.reset = true;
|
self.reset = true;
|
||||||
|
@ -683,10 +662,7 @@ impl Plot {
|
||||||
show_axes,
|
show_axes,
|
||||||
linked_axes,
|
linked_axes,
|
||||||
linked_cursors,
|
linked_cursors,
|
||||||
|
|
||||||
clamp_grid,
|
|
||||||
grid_spacers,
|
grid_spacers,
|
||||||
sharp_grid_lines,
|
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Determine the size of the plot in the UI
|
// Determine the size of the plot in the UI
|
||||||
|
@ -742,7 +718,7 @@ impl Plot {
|
||||||
});
|
});
|
||||||
|
|
||||||
let PlotMemory {
|
let PlotMemory {
|
||||||
bounds_modified,
|
mut bounds_modified,
|
||||||
mut hovered_entry,
|
mut hovered_entry,
|
||||||
mut hidden_items,
|
mut hidden_items,
|
||||||
last_screen_transform,
|
last_screen_transform,
|
||||||
|
@ -754,7 +730,6 @@ 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(),
|
||||||
};
|
};
|
||||||
|
@ -763,7 +738,6 @@ impl Plot {
|
||||||
mut items,
|
mut items,
|
||||||
mut response,
|
mut response,
|
||||||
last_screen_transform,
|
last_screen_transform,
|
||||||
mut bounds_modified,
|
|
||||||
..
|
..
|
||||||
} = plot_ui;
|
} = plot_ui;
|
||||||
|
|
||||||
|
@ -955,9 +929,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(|i| i.zoom_delta()))
|
Vec2::splat(ui.input().zoom_delta())
|
||||||
} else {
|
} else {
|
||||||
ui.input(|i| i.zoom_delta_2d())
|
ui.input().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);
|
||||||
|
@ -965,7 +939,7 @@ impl Plot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allow_scroll {
|
if allow_scroll {
|
||||||
let scroll_delta = ui.input(|i| i.scroll_delta);
|
let scroll_delta = ui.input().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();
|
||||||
|
@ -987,12 +961,10 @@ impl Plot {
|
||||||
axis_formatters,
|
axis_formatters,
|
||||||
show_axes,
|
show_axes,
|
||||||
transform: transform.clone(),
|
transform: transform.clone(),
|
||||||
|
grid_spacers,
|
||||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x),
|
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x),
|
||||||
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
|
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
|
||||||
draw_cursors,
|
draw_cursors,
|
||||||
grid_spacers,
|
|
||||||
sharp_grid_lines,
|
|
||||||
clamp_grid,
|
|
||||||
};
|
};
|
||||||
let plot_cursors = prepared.ui(ui, &response);
|
let plot_cursors = prepared.ui(ui, &response);
|
||||||
|
|
||||||
|
@ -1044,7 +1016,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
@ -1072,13 +1043,11 @@ 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.
|
||||||
|
@ -1099,7 +1068,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(|i| i.pointer.latest_pos())? - self.response.drag_delta();
|
let last_pos = self.ctx().input().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)
|
||||||
}
|
}
|
||||||
|
@ -1317,36 +1286,22 @@ struct PreparedPlot {
|
||||||
axis_formatters: [AxisFormatter; 2],
|
axis_formatters: [AxisFormatter; 2],
|
||||||
show_axes: [bool; 2],
|
show_axes: [bool; 2],
|
||||||
transform: ScreenTransform,
|
transform: ScreenTransform,
|
||||||
|
grid_spacers: [GridSpacer; 2],
|
||||||
draw_cursor_x: bool,
|
draw_cursor_x: bool,
|
||||||
draw_cursor_y: bool,
|
draw_cursor_y: bool,
|
||||||
draw_cursors: Vec<Cursor>,
|
draw_cursors: Vec<Cursor>,
|
||||||
|
|
||||||
grid_spacers: [GridSpacer; 2],
|
|
||||||
sharp_grid_lines: bool,
|
|
||||||
clamp_grid: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedPlot {
|
impl PreparedPlot {
|
||||||
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
||||||
let mut axes_shapes = Vec::new();
|
let mut shapes = Vec::new();
|
||||||
|
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if self.show_axes[d] {
|
if self.show_axes[d] {
|
||||||
self.paint_axis(
|
self.paint_axis(ui, d, &mut shapes);
|
||||||
ui,
|
|
||||||
d,
|
|
||||||
self.show_axes[1 - d],
|
|
||||||
&mut axes_shapes,
|
|
||||||
self.sharp_grid_lines,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the axes by strength so that those with higher strength are drawn in front.
|
|
||||||
axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
|
|
||||||
|
|
||||||
let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
|
|
||||||
|
|
||||||
let transform = &self.transform;
|
let transform = &self.transform;
|
||||||
|
|
||||||
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
||||||
|
@ -1414,21 +1369,11 @@ impl PreparedPlot {
|
||||||
cursors
|
cursors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_axis(
|
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||||
&self,
|
|
||||||
ui: &Ui,
|
|
||||||
axis: usize,
|
|
||||||
other_axis_shown: bool,
|
|
||||||
shapes: &mut Vec<(Shape, f32)>,
|
|
||||||
sharp_grid_lines: bool,
|
|
||||||
) {
|
|
||||||
#![allow(clippy::collapsible_else_if)]
|
|
||||||
|
|
||||||
let Self {
|
let Self {
|
||||||
transform,
|
transform,
|
||||||
axis_formatters,
|
axis_formatters,
|
||||||
grid_spacers,
|
grid_spacers,
|
||||||
clamp_grid,
|
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -1442,6 +1387,7 @@ impl PreparedPlot {
|
||||||
let font_id = TextStyle::Body.resolve(ui.style());
|
let font_id = TextStyle::Body.resolve(ui.style());
|
||||||
|
|
||||||
// Where on the cross-dimension to show the label values
|
// Where on the cross-dimension to show the label values
|
||||||
|
let bounds = transform.bounds();
|
||||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||||
|
|
||||||
let input = GridInput {
|
let input = GridInput {
|
||||||
|
@ -1450,31 +1396,9 @@ impl PreparedPlot {
|
||||||
};
|
};
|
||||||
let steps = (grid_spacers[axis])(input);
|
let steps = (grid_spacers[axis])(input);
|
||||||
|
|
||||||
let clamp_range = clamp_grid.then(|| {
|
|
||||||
let mut tight_bounds = PlotBounds::NOTHING;
|
|
||||||
for item in &self.items {
|
|
||||||
let item_bounds = item.bounds();
|
|
||||||
tight_bounds.merge_x(&item_bounds);
|
|
||||||
tight_bounds.merge_y(&item_bounds);
|
|
||||||
}
|
|
||||||
tight_bounds
|
|
||||||
});
|
|
||||||
|
|
||||||
for step in steps {
|
for step in steps {
|
||||||
let value_main = step.value;
|
let value_main = step.value;
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
|
||||||
if axis == 0 {
|
|
||||||
if !clamp_range.range_x().contains(&value_main) {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
if !clamp_range.range_y().contains(&value_main) {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = if axis == 0 {
|
let value = if axis == 0 {
|
||||||
PlotPoint::new(value_main, value_cross)
|
PlotPoint::new(value_main, value_cross)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1484,47 +1408,29 @@ impl PreparedPlot {
|
||||||
let pos_in_gui = transform.position_from_point(&value);
|
let pos_in_gui = transform.position_from_point(&value);
|
||||||
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
||||||
|
|
||||||
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
let line_alpha = remap_clamp(
|
||||||
let line_strength = remap_clamp(
|
|
||||||
spacing_in_points,
|
spacing_in_points,
|
||||||
MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
|
(MIN_LINE_SPACING_IN_POINTS as f32)..=300.0,
|
||||||
0.0..=1.0,
|
0.0..=0.15,
|
||||||
);
|
);
|
||||||
|
|
||||||
let line_color = color_from_contrast(ui, line_strength);
|
if line_alpha > 0.0 {
|
||||||
|
let line_color = color_from_alpha(ui, line_alpha);
|
||||||
|
|
||||||
let mut p0 = pos_in_gui;
|
let mut p0 = pos_in_gui;
|
||||||
let mut p1 = pos_in_gui;
|
let mut p1 = pos_in_gui;
|
||||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
p0[1 - axis] = transform.frame().min[1 - axis];
|
||||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
p1[1 - axis] = transform.frame().max[1 - axis];
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
|
||||||
if axis == 0 {
|
|
||||||
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
|
||||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
|
||||||
} else {
|
|
||||||
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
|
||||||
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sharp_grid_lines {
|
|
||||||
// Round to avoid aliasing
|
// Round to avoid aliasing
|
||||||
p0 = ui.ctx().round_pos_to_pixels(p0);
|
p0 = ui.ctx().round_pos_to_pixels(p0);
|
||||||
p1 = ui.ctx().round_pos_to_pixels(p1);
|
p1 = ui.ctx().round_pos_to_pixels(p1);
|
||||||
|
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
|
||||||
}
|
}
|
||||||
|
|
||||||
shapes.push((
|
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
|
||||||
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
|
|
||||||
line_strength,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
if text_alpha > 0.0 {
|
||||||
if spacing_in_points > MIN_TEXT_SPACING {
|
let color = color_from_alpha(ui, text_alpha);
|
||||||
let text_strength =
|
|
||||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
|
||||||
let color = color_from_contrast(ui, text_strength);
|
|
||||||
|
|
||||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||||
formatter(value_main, &axis_range)
|
formatter(value_main, &axis_range)
|
||||||
|
@ -1532,11 +1438,8 @@ impl PreparedPlot {
|
||||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
|
||||||
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
|
||||||
|
|
||||||
// Custom formatters can return empty string to signal "no label at this resolution"
|
// Custom formatters can return empty string to signal "no label at this resolution"
|
||||||
if !text.is_empty() && !skip_origin_y {
|
if !text.is_empty() {
|
||||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
||||||
|
|
||||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||||
|
@ -1546,20 +1449,17 @@ impl PreparedPlot {
|
||||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
.at_least(transform.frame().min[1 - axis] + 1.0);
|
||||||
|
|
||||||
shapes.push((Shape::galley(text_pos, galley), text_strength));
|
shapes.push(Shape::galley(text_pos, galley));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
|
||||||
let bg = ui.visuals().extreme_bg_color;
|
if ui.visuals().dark_mode {
|
||||||
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
Rgba::from_white_alpha(alpha).into()
|
||||||
let mix = 0.5 * contrast.sqrt();
|
} else {
|
||||||
Color32::from_rgb(
|
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
|
||||||
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
}
|
||||||
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
|
||||||
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1652,16 +1552,3 @@ fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64,
|
||||||
});
|
});
|
||||||
out.extend(marks_iter);
|
out.extend(marks_iter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper for formatting a number so that we always show at least a few decimals,
|
|
||||||
/// unless it is an integer, in which case we never show any decimals.
|
|
||||||
pub fn format_number(number: f64, num_decimals: usize) -> String {
|
|
||||||
let is_integral = number as i64 as f64 == number;
|
|
||||||
if is_integral {
|
|
||||||
// perfect integer - show it as such:
|
|
||||||
format!("{:.0}", number)
|
|
||||||
} else {
|
|
||||||
// make sure we tell the user it is not an integer by always showing a decimal or two:
|
|
||||||
format!("{:.*}", num_decimals.at_least(1), number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -224,7 +224,6 @@ impl ScreenTransform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ui-space rectangle.
|
|
||||||
pub fn frame(&self) -> &Rect {
|
pub fn frame(&self) -> &Rect {
|
||||||
&self.frame
|
&self.frame
|
||||||
}
|
}
|
||||||
|
@ -264,27 +263,18 @@ impl ScreenTransform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn position_from_point_x(&self, value: f64) -> f32 {
|
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
|
||||||
remap(
|
let x = remap(
|
||||||
value,
|
value.x,
|
||||||
self.bounds.min[0]..=self.bounds.max[0],
|
self.bounds.min[0]..=self.bounds.max[0],
|
||||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||||
) as f32
|
);
|
||||||
}
|
let y = remap(
|
||||||
|
value.y,
|
||||||
pub fn position_from_point_y(&self, value: f64) -> f32 {
|
|
||||||
remap(
|
|
||||||
value,
|
|
||||||
self.bounds.min[1]..=self.bounds.max[1],
|
self.bounds.min[1]..=self.bounds.max[1],
|
||||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||||
) as f32
|
);
|
||||||
}
|
pos2(x as f32, y as f32)
|
||||||
|
|
||||||
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
|
|
||||||
pos2(
|
|
||||||
self.position_from_point_x(value.x),
|
|
||||||
self.position_from_point_y(value.y),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
|
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
|
||||||
|
|
|
@ -13,7 +13,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,12 +33,6 @@ 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()));
|
||||||
|
@ -68,7 +60,6 @@ impl Widget for ProgressBar {
|
||||||
progress,
|
progress,
|
||||||
desired_width,
|
desired_width,
|
||||||
text,
|
text,
|
||||||
fill,
|
|
||||||
animate,
|
animate,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -87,8 +78,12 @@ impl Widget for ProgressBar {
|
||||||
|
|
||||||
let visuals = ui.style().visuals.clone();
|
let visuals = ui.style().visuals.clone();
|
||||||
let rounding = outer_rect.height() / 2.0;
|
let rounding = outer_rect.height() / 2.0;
|
||||||
ui.painter()
|
ui.painter().rect(
|
||||||
.rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
|
outer_rect,
|
||||||
|
rounding,
|
||||||
|
visuals.extreme_bg_color,
|
||||||
|
Stroke::none(),
|
||||||
|
);
|
||||||
let inner_rect = Rect::from_min_size(
|
let inner_rect = Rect::from_min_size(
|
||||||
outer_rect.min,
|
outer_rect.min,
|
||||||
vec2(
|
vec2(
|
||||||
|
@ -99,8 +94,7 @@ 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 {
|
||||||
let time = ui.input(|i| i.time);
|
lerp(dark..=bright, ui.input().time.cos().abs())
|
||||||
lerp(dark..=bright, time.cos().abs())
|
|
||||||
} else {
|
} else {
|
||||||
bright
|
bright
|
||||||
};
|
};
|
||||||
|
@ -108,17 +102,14 @@ impl Widget for ProgressBar {
|
||||||
ui.painter().rect(
|
ui.painter().rect(
|
||||||
inner_rect,
|
inner_rect,
|
||||||
rounding,
|
rounding,
|
||||||
Color32::from(
|
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
|
||||||
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 time = ui.input(|i| i.time);
|
let start_angle = ui.input().time * std::f64::consts::TAU;
|
||||||
let start_angle = time * std::f64::consts::TAU;
|
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
||||||
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| {
|
||||||
|
@ -129,8 +120,10 @@ impl Widget for ProgressBar {
|
||||||
+ vec2(-rounding, 0.0)
|
+ vec2(-rounding, 0.0)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
ui.painter()
|
ui.painter().add(Shape::line(
|
||||||
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
|
points,
|
||||||
|
Stroke::new(2.0, visuals.faint_bg_color),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(text_kind) = text {
|
if let Some(text_kind) = text {
|
||||||
|
|
|
@ -61,15 +61,11 @@ 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.highlighted() || response.has_focus() {
|
if selected || response.hovered() || response.has_focus() {
|
||||||
let rect = rect.expand(visuals.expansion);
|
let rect = rect.expand(visuals.expansion);
|
||||||
|
|
||||||
ui.painter().rect(
|
ui.painter()
|
||||||
rect,
|
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
|
||||||
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,7 +14,6 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +21,6 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,19 +28,12 @@ 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 {
|
||||||
|
@ -51,40 +42,18 @@ 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;
|
||||||
|
|
||||||
|
@ -103,19 +72,10 @@ impl Widget for Separator {
|
||||||
|
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
||||||
let painter = ui.painter();
|
|
||||||
if is_horizontal_line {
|
if is_horizontal_line {
|
||||||
painter.hline(
|
ui.painter().hline(rect.x_range(), rect.center().y, stroke);
|
||||||
(rect.left() - grow)..=(rect.right() + grow),
|
|
||||||
painter.round_to_pixel(rect.center().y),
|
|
||||||
stroke,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
painter.vline(
|
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
|
||||||
painter.round_to_pixel(rect.center().x),
|
|
||||||
(rect.top() - grow)..=(rect.bottom() + grow),
|
|
||||||
stroke,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,12 +79,10 @@ pub struct Slider<'a> {
|
||||||
text: WidgetText,
|
text: WidgetText,
|
||||||
/// Sets the minimal step of the widget value
|
/// Sets the minimal step of the widget value
|
||||||
step: Option<f64>,
|
step: Option<f64>,
|
||||||
drag_value_speed: Option<f64>,
|
|
||||||
min_decimals: usize,
|
min_decimals: usize,
|
||||||
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> {
|
||||||
|
@ -125,12 +123,10 @@ impl<'a> Slider<'a> {
|
||||||
suffix: Default::default(),
|
suffix: Default::default(),
|
||||||
text: Default::default(),
|
text: Default::default(),
|
||||||
step: None,
|
step: None,
|
||||||
drag_value_speed: None,
|
|
||||||
min_decimals: 0,
|
min_decimals: 0,
|
||||||
max_decimals: None,
|
max_decimals: None,
|
||||||
custom_formatter: None,
|
custom_formatter: None,
|
||||||
custom_parser: None,
|
custom_parser: None,
|
||||||
trailing_fill: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +212,6 @@ impl<'a> Slider<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the minimal change of the value.
|
/// Sets the minimal change of the value.
|
||||||
///
|
|
||||||
/// Value `0.0` effectively disables the feature. If the new value is out of range
|
/// Value `0.0` effectively disables the feature. If the new value is out of range
|
||||||
/// and `clamp_to_range` is enabled, you would not have the ability to change the value.
|
/// and `clamp_to_range` is enabled, you would not have the ability to change the value.
|
||||||
///
|
///
|
||||||
|
@ -226,22 +221,8 @@ impl<'a> Slider<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When dragging the value, how fast does it move?
|
|
||||||
///
|
|
||||||
/// Unit: values per point (logical pixel).
|
|
||||||
/// See also [`DragValue::speed`].
|
|
||||||
///
|
|
||||||
/// By default this is the same speed as when dragging the slider,
|
|
||||||
/// but you can change it here to for instance have a much finer control
|
|
||||||
/// by dragging the slider value rather than the slider itself.
|
|
||||||
pub fn drag_value_speed(mut self, drag_value_speed: f64) -> Self {
|
|
||||||
self.drag_value_speed = Some(drag_value_speed);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(emilk): we should also have a "min precision".
|
// TODO(emilk): we should also have a "min precision".
|
||||||
/// Set a minimum number of decimals to display.
|
/// Set a minimum number of decimals to display.
|
||||||
///
|
|
||||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||||
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
|
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
|
||||||
|
@ -251,7 +232,6 @@ impl<'a> Slider<'a> {
|
||||||
|
|
||||||
// TODO(emilk): we should also have a "max precision".
|
// TODO(emilk): we should also have a "max precision".
|
||||||
/// Set a maximum number of decimals to display.
|
/// Set a maximum number of decimals to display.
|
||||||
///
|
|
||||||
/// Values will also be rounded to this number of decimals.
|
/// Values will also be rounded to this number of decimals.
|
||||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||||
|
@ -261,7 +241,6 @@ impl<'a> Slider<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set an exact number of decimals to display.
|
/// Set an exact number of decimals to display.
|
||||||
///
|
|
||||||
/// Values will also be rounded to this number of decimals.
|
/// Values will also be rounded to this number of decimals.
|
||||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||||
|
@ -271,17 +250,6 @@ 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
|
||||||
|
@ -482,7 +450,7 @@ impl<'a> Slider<'a> {
|
||||||
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
||||||
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
||||||
pub fn integer(self) -> Self {
|
pub fn integer(self) -> Self {
|
||||||
self.fixed_decimals(0).smallest_positive(1.0).step_by(1.0)
|
self.fixed_decimals(0).smallest_positive(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_value(&mut self) -> f64 {
|
fn get_value(&mut self) -> f64 {
|
||||||
|
@ -542,7 +510,7 @@ impl<'a> Slider<'a> {
|
||||||
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
||||||
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
|
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
|
||||||
};
|
};
|
||||||
ui.allocate_response(desired_size, Sense::drag())
|
ui.allocate_response(desired_size, Sense::click_and_drag())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Just the slider, no text
|
/// Just the slider, no text
|
||||||
|
@ -553,7 +521,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(|i| i.aim_radius());
|
let aim_radius = ui.input().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()),
|
||||||
|
@ -564,9 +532,6 @@ impl<'a> Slider<'a> {
|
||||||
self.set_value(new_value);
|
self.set_value(new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut decrement = 0usize;
|
|
||||||
let mut increment = 0usize;
|
|
||||||
|
|
||||||
if response.has_focus() {
|
if response.has_focus() {
|
||||||
let (dec_key, inc_key) = match self.orientation {
|
let (dec_key, inc_key) = match self.orientation {
|
||||||
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
||||||
|
@ -575,21 +540,8 @@ impl<'a> Slider<'a> {
|
||||||
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.input(|input| {
|
let decrement = ui.input().num_presses(dec_key);
|
||||||
decrement += input.num_presses(dec_key);
|
let increment = ui.input().num_presses(inc_key);
|
||||||
increment += input.num_presses(inc_key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
use accesskit::Action;
|
|
||||||
ui.input(|input| {
|
|
||||||
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
|
|
||||||
increment += 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;
|
||||||
|
|
||||||
if kb_step != 0.0 {
|
if kb_step != 0.0 {
|
||||||
|
@ -599,27 +551,22 @@ 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(|i| i.aim_radius());
|
let aim_radius = ui.input().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(
|
||||||
self.value_from_position(new_position + aim_radius, position_range.clone()),
|
new_position - aim_radius,
|
||||||
|
position_range.clone(),
|
||||||
|
),
|
||||||
|
self.value_from_position(
|
||||||
|
new_position + aim_radius,
|
||||||
|
position_range.clone(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => self.value_from_position(new_position, position_range.clone()),
|
_ => self.value_from_position(new_position, position_range.clone()),
|
||||||
};
|
};
|
||||||
self.set_value(new_value);
|
self.set_value(new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
use accesskit::{Action, ActionData};
|
|
||||||
ui.input(|input| {
|
|
||||||
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
|
|
||||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
|
||||||
self.set_value(new_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint it:
|
// Paint it:
|
||||||
|
@ -629,40 +576,22 @@ 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 visuals = ui.style().interact(response);
|
|
||||||
let widget_visuals = &ui.visuals().widgets;
|
|
||||||
|
|
||||||
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 position_1d = self.position_from_value(value, position_range);
|
||||||
|
|
||||||
|
let visuals = ui.style().interact(response);
|
||||||
|
ui.painter().add(epaint::RectShape {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
||||||
|
@ -728,21 +657,18 @@ 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 {
|
||||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||||
let change = ui.input(|input| {
|
let change = {
|
||||||
|
// Hold one lock rather than 4 (see https://github.com/emilk/egui/pull/1380).
|
||||||
|
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 speed = if let (Some(step), true) = (self.step, any_change) {
|
|
||||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
|
||||||
step
|
|
||||||
} else {
|
|
||||||
self.drag_value_speed
|
|
||||||
.unwrap_or_else(|| self.current_gradient(&position_range))
|
|
||||||
};
|
};
|
||||||
|
let speed = match self.step {
|
||||||
|
Some(step) if change != 0 => step,
|
||||||
|
_ => self.current_gradient(&position_range),
|
||||||
|
};
|
||||||
let mut value = self.get_value();
|
let mut value = self.get_value();
|
||||||
let response = ui.add({
|
let response = ui.add({
|
||||||
let mut dv = DragValue::new(&mut value)
|
let mut dv = DragValue::new(&mut value)
|
||||||
|
@ -779,39 +705,13 @@ impl<'a> Slider<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
||||||
let old_value = self.get_value();
|
|
||||||
|
|
||||||
let thickness = ui
|
let thickness = ui
|
||||||
.text_style_height(&TextStyle::Body)
|
.text_style_height(&TextStyle::Body)
|
||||||
.at_least(ui.spacing().interact_size.y);
|
.at_least(ui.spacing().interact_size.y);
|
||||||
let mut response = self.allocate_slider_space(ui, thickness);
|
let mut response = self.allocate_slider_space(ui, thickness);
|
||||||
self.slider_ui(ui, &response);
|
self.slider_ui(ui, &response);
|
||||||
|
|
||||||
let value = self.get_value();
|
if self.show_value {
|
||||||
response.changed = value != old_value;
|
|
||||||
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
|
||||||
use accesskit::Action;
|
|
||||||
builder.set_min_numeric_value(*self.range.start());
|
|
||||||
builder.set_max_numeric_value(*self.range.end());
|
|
||||||
if let Some(step) = self.step {
|
|
||||||
builder.set_numeric_value_step(step);
|
|
||||||
}
|
|
||||||
builder.add_action(Action::SetValue);
|
|
||||||
let clamp_range = self.clamp_range();
|
|
||||||
if value < *clamp_range.end() {
|
|
||||||
builder.add_action(Action::Increment);
|
|
||||||
}
|
|
||||||
if value > *clamp_range.start() {
|
|
||||||
builder.add_action(Action::Decrement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let slider_response = response.clone();
|
|
||||||
|
|
||||||
let value_response = if self.show_value {
|
|
||||||
let position_range = self.position_range(&response.rect);
|
let position_range = self.position_range(&response.rect);
|
||||||
let value_response = self.value_ui(ui, position_range);
|
let value_response = self.value_ui(ui, position_range);
|
||||||
if value_response.gained_focus()
|
if value_response.gained_focus()
|
||||||
|
@ -823,23 +723,12 @@ impl<'a> Slider<'a> {
|
||||||
response = value_response.union(response);
|
response = value_response.union(response);
|
||||||
} else {
|
} else {
|
||||||
// Use the slider id as the id for the whole widget
|
// Use the slider id as the id for the whole widget
|
||||||
response = response.union(value_response.clone());
|
response = response.union(value_response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(value_response)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.text.is_empty() {
|
if !self.text.is_empty() {
|
||||||
let label_response = ui.add(Label::new(self.text.clone()).wrap(false));
|
ui.add(Label::new(self.text.clone()).wrap(false));
|
||||||
// The slider already has an accessibility label via widget info,
|
|
||||||
// but sometimes it's useful for a screen reader to know
|
|
||||||
// that a piece of text is a label for another widget,
|
|
||||||
// e.g. so the text itself can be excluded from navigation.
|
|
||||||
slider_response.labelled_by(label_response.id);
|
|
||||||
if let Some(value_response) = value_response {
|
|
||||||
value_response.labelled_by(label_response.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -848,12 +737,18 @@ impl<'a> Slider<'a> {
|
||||||
|
|
||||||
impl<'a> Widget for Slider<'a> {
|
impl<'a> Widget for Slider<'a> {
|
||||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||||
|
let old_value = self.get_value();
|
||||||
|
|
||||||
let inner_response = match self.orientation {
|
let inner_response = match self.orientation {
|
||||||
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
||||||
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
||||||
};
|
};
|
||||||
|
|
||||||
inner_response.inner | inner_response.response
|
let mut response = inner_response.inner | inner_response.response;
|
||||||
|
let value = self.get_value();
|
||||||
|
response.changed = value != old_value;
|
||||||
|
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
||||||
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,8 @@ 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 time = ui.input(|i| i.time);
|
let start_angle = ui.input().time * std::f64::consts::TAU;
|
||||||
let start_angle = time * std::f64::consts::TAU;
|
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
||||||
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(|i| i.key_pressed(egui::Key::Enter)) {
|
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
||||||
/// // …
|
/// // …
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
|
@ -67,9 +67,6 @@ 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> {
|
||||||
|
@ -92,7 +89,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,9 +112,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +206,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(|f| f.layout_job(layout_job))
|
/// ui.fonts().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));
|
||||||
/// # });
|
/// # });
|
||||||
|
@ -276,37 +269,6 @@ 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -350,7 +312,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_mut(|mem| mem.request_focus(output.response.id));
|
ui.memory().request_focus(output.response.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if frame {
|
if frame {
|
||||||
|
@ -402,16 +364,13 @@ 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
|
||||||
|
@ -422,7 +381,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(|f| f.row_height(&font_id));
|
let row_height = ui.fonts().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);
|
||||||
|
@ -430,31 +389,29 @@ 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);
|
||||||
let layout_job = if multiline {
|
ui.fonts().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 clip_text {
|
let desired_width = if multiline {
|
||||||
wrap_width // visual clipping with scroll in singleline input.
|
galley.size().x.max(wrap_width) // always show everything in multiline
|
||||||
} else {
|
} else {
|
||||||
galley.size().x.max(wrap_width)
|
wrap_width // visual clipping with scroll in singleline input. TODO(emilk): opt-in/out?
|
||||||
};
|
};
|
||||||
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);
|
||||||
|
|
||||||
|
@ -471,8 +428,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 allow_drag_to_select =
|
let any_touches = ui.input().any_touches(); // separate line to avoid double-locking the same mutex
|
||||||
ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id));
|
let allow_drag_to_select = !any_touches || ui.memory().has_focus(id);
|
||||||
|
|
||||||
let sense = if interactive {
|
let sense = if interactive {
|
||||||
if allow_drag_to_select {
|
if allow_drag_to_select {
|
||||||
|
@ -490,7 +447,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_mut(|o| o.mutable_text_under_cursor = true);
|
ui.output().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)
|
||||||
|
@ -500,7 +457,7 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
if ui.visuals().text_cursor_preview
|
if ui.visuals().text_cursor_preview
|
||||||
&& response.hovered()
|
&& response.hovered()
|
||||||
&& ui.input(|i| i.pointer.is_moving())
|
&& ui.input().pointer.is_moving()
|
||||||
{
|
{
|
||||||
// preview:
|
// preview:
|
||||||
paint_cursor_end(
|
paint_cursor_end(
|
||||||
|
@ -530,9 +487,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(|i| i.pointer.any_pressed()) {
|
if response.hovered() && ui.input().pointer.any_pressed() {
|
||||||
ui.memory_mut(|mem| mem.request_focus(id));
|
ui.memory().request_focus(id);
|
||||||
if ui.input(|i| i.modifiers.shift) {
|
if ui.input().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));
|
||||||
|
@ -542,8 +499,7 @@ 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(|i| i.pointer.any_down())
|
} else if ui.input().pointer.any_down() && response.is_pointer_button_down_on()
|
||||||
&& 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) {
|
||||||
|
@ -555,14 +511,14 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && response.hovered() {
|
if response.hovered() && interactive {
|
||||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
ui.output().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 interactive && ui.memory(|mem| mem.has_focus(id)) {
|
if ui.memory().has_focus(id) && interactive {
|
||||||
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
|
ui.memory().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())
|
||||||
|
@ -589,15 +545,11 @@ impl<'t> TextEdit<'t> {
|
||||||
cursor_range = Some(new_cursor_range);
|
cursor_range = Some(new_cursor_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text_draw_pos = align
|
let mut text_draw_pos = response.rect.min;
|
||||||
.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 clip_text && align_offset == 0.0 {
|
if !multiline {
|
||||||
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
|
let cursor_pos = match (cursor_range, ui.memory().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,
|
||||||
};
|
};
|
||||||
|
@ -619,8 +571,6 @@ 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)) =
|
||||||
|
@ -644,7 +594,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(|mem| mem.has_focus(id)) {
|
if ui.memory().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).
|
||||||
|
@ -671,13 +621,9 @@ 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_mut(|o| {
|
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
|
||||||
o.text_cursor_pos = Some(cursor_pos.left_top());
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
ui.ctx().output_mut(|o| {
|
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_bottom());
|
||||||
o.text_cursor_pos = Some(cursor_pos.left_bottom());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -702,7 +648,11 @@ impl<'t> TextEdit<'t> {
|
||||||
char_range,
|
char_range,
|
||||||
mask_if_password(password, text.as_str()),
|
mask_if_password(password, text.as_str()),
|
||||||
);
|
);
|
||||||
response.output_event(OutputEvent::TextSelectionChanged(info));
|
response
|
||||||
|
.ctx
|
||||||
|
.output()
|
||||||
|
.events
|
||||||
|
.push(OutputEvent::TextSelectionChanged(info));
|
||||||
} else {
|
} else {
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
WidgetInfo::text_edit(
|
WidgetInfo::text_edit(
|
||||||
|
@ -712,103 +662,6 @@ impl<'t> TextEdit<'t> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
{
|
|
||||||
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| {
|
|
||||||
use accesskit::{TextPosition, TextSelection};
|
|
||||||
|
|
||||||
let parent_id = response.id;
|
|
||||||
|
|
||||||
if let Some(cursor_range) = &cursor_range {
|
|
||||||
let anchor = &cursor_range.secondary.rcursor;
|
|
||||||
let focus = &cursor_range.primary.rcursor;
|
|
||||||
builder.set_text_selection(TextSelection {
|
|
||||||
anchor: TextPosition {
|
|
||||||
node: parent_id.with(anchor.row).accesskit_id(),
|
|
||||||
character_index: anchor.column,
|
|
||||||
},
|
|
||||||
focus: TextPosition {
|
|
||||||
node: parent_id.with(focus.row).accesskit_id(),
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
response,
|
response,
|
||||||
galley,
|
galley,
|
||||||
|
@ -836,28 +689,6 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
fn ccursor_from_accesskit_text_position(
|
|
||||||
id: Id,
|
|
||||||
galley: &Galley,
|
|
||||||
position: &accesskit::TextPosition,
|
|
||||||
) -> Option<CCursor> {
|
|
||||||
let mut total_length = 0usize;
|
|
||||||
for (i, row) in galley.rows.iter().enumerate() {
|
|
||||||
let row_id = id.with(i);
|
|
||||||
if row_id.accesskit_id() == position.node {
|
|
||||||
return Some(CCursor {
|
|
||||||
index: total_length + position.character_index,
|
|
||||||
prefer_next_row: !(position.character_index == row.glyphs.len()
|
|
||||||
&& !row.ends_with_newline
|
|
||||||
&& (i + 1) < galley.rows.len()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check for (keyboard) events to edit the cursor and/or text.
|
/// Check for (keyboard) events to edit the cursor and/or text.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn events(
|
fn events(
|
||||||
|
@ -877,19 +708,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(|i| i.time),
|
ui.input().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_mut(|o| o.copied_text = text);
|
ui.ctx().output().copied_text = text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut any_change = false;
|
let mut any_change = false;
|
||||||
|
|
||||||
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
|
let events = ui.input().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 => {
|
||||||
|
@ -932,9 +763,8 @@ fn events(
|
||||||
key: Key::Tab,
|
key: Key::Tab,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
|
if multiline && ui.memory().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?
|
||||||
|
@ -958,7 +788,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_mut(|mem| mem.surrender_focus(id)); // End input with enter
|
ui.memory().surrender_focus(id); // End input with enter
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -966,7 +796,6 @@ fn events(
|
||||||
key: Key::Z,
|
key: Key::Z,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
|
||||||
} if modifiers.command && !modifiers.shift => {
|
} if modifiers.command && !modifiers.shift => {
|
||||||
// TODO(emilk): redo
|
// TODO(emilk): redo
|
||||||
if let Some((undo_ccursor_range, undo_txt)) = state
|
if let Some((undo_ccursor_range, undo_txt)) = state
|
||||||
|
@ -985,7 +814,6 @@ fn events(
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
|
||||||
} => on_key_press(&mut cursor_range, text, galley, *key, modifiers),
|
} => on_key_press(&mut cursor_range, text, galley, *key, modifiers),
|
||||||
|
|
||||||
Event::CompositionStart => {
|
Event::CompositionStart => {
|
||||||
|
@ -1021,27 +849,6 @@ fn events(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
|
||||||
action: accesskit::Action::SetTextSelection,
|
|
||||||
target,
|
|
||||||
data: Some(accesskit::ActionData::SetTextSelection(selection)),
|
|
||||||
}) => {
|
|
||||||
if id.accesskit_id() == *target {
|
|
||||||
let primary =
|
|
||||||
ccursor_from_accesskit_text_position(id, galley, &selection.focus);
|
|
||||||
let secondary =
|
|
||||||
ccursor_from_accesskit_text_position(id, galley, &selection.anchor);
|
|
||||||
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
|
||||||
Some(CCursorRange { primary, secondary })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1062,7 +869,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(|i| i.time),
|
ui.input().time,
|
||||||
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use epaint::text::cursor::*;
|
use epaint::text::cursor::*;
|
||||||
|
|
||||||
/// A selected text range (could be a range of length zero).
|
/// A selected text range (could be a range of length zero).
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct CursorRange {
|
pub struct CursorRange {
|
||||||
/// When selecting with a mouse, this is where the mouse was released.
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
|
|
|
@ -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_mut(|d| d.get_persisted(id))
|
ctx.data().get_persisted(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
ctx.data().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.21.0"
|
version = "0.19.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"]
|
||||||
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
screen_reader = ["eframe/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.21.0", path = "../eframe", default-features = false }
|
eframe = { version = "0.19.0", path = "../eframe", default-features = false }
|
||||||
egui = { version = "0.21.0", path = "../egui", features = [
|
egui = { version = "0.19.0", path = "../egui", features = [
|
||||||
"extra_debug_asserts",
|
"extra_debug_asserts",
|
||||||
] }
|
] }
|
||||||
egui_demo_lib = { version = "0.21.0", path = "../egui_demo_lib", features = [
|
egui_demo_lib = { version = "0.19.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.21.0", optional = true, path = "../egui_extras" }
|
egui_extras = { version = "0.19.0", optional = true, path = "../egui_extras" }
|
||||||
|
|
||||||
# feature "http":
|
# feature "http":
|
||||||
ehttp = { version = "0.2.0", optional = true }
|
ehttp = { version = "0.2.0", optional = true }
|
||||||
|
@ -50,7 +50,7 @@ image = { version = "0.24", optional = true, default-features = false, features
|
||||||
"jpeg",
|
"jpeg",
|
||||||
"png",
|
"png",
|
||||||
] }
|
] }
|
||||||
poll-promise = { version = "0.2", optional = true, default-features = false }
|
poll-promise = { version = "0.1", optional = true, default-features = false }
|
||||||
|
|
||||||
# feature "persistence":
|
# feature "persistence":
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
|
|
|
@ -146,12 +146,12 @@ impl RotatingTriangle {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gl.compile_shader(shader);
|
gl.compile_shader(shader);
|
||||||
assert!(
|
if !gl.get_shader_compile_status(shader) {
|
||||||
gl.get_shader_compile_status(shader),
|
panic!(
|
||||||
"Failed to compile custom_3d_glow {shader_type}: {}",
|
"Failed to compile custom_3d_glow: {}",
|
||||||
gl.get_shader_info_log(shader)
|
gl.get_shader_info_log(shader)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
gl.attach_shader(program, shader);
|
gl.attach_shader(program, shader);
|
||||||
shader
|
shader
|
||||||
})
|
})
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue