Compare commits

..

3 commits

Author SHA1 Message Date
Emil Ernerfeldt
77ba7d5163 Make sure the minimize button fits in the example 2023-01-23 09:27:31 +01:00
SunDoge
3e155c9f41 update custom_window_frame example 2022-12-27 21:25:01 +08:00
SunDoge
7c2b2149e3 add set_minimized for Frame 2022-12-27 21:18:04 +08:00
181 changed files with 4045 additions and 4737 deletions

View file

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

View file

@ -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

3
.gitignore vendored
View file

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

View file

@ -5,48 +5,21 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased ## Unreleased
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
### Added ⭐ ### Added ⭐
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)).
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). * Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
* Add `Response::on_hover_and_drag_cursor`. * Add `Response::on_hover_and_drag_cursor`.
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
### Changed 🔧 ### Changed 🔧
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`. * Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
* `DragValue` and `Slider` now use the proportional font ([#2638](https://github.com/emilk/egui/pull/2638)).
* `ScrollArea` is less aggressive about clipping its contents ([#2665](https://github.com/emilk/egui/pull/2665)).
* Updated to be compatible with a major breaking change in AccessKit that drastically reduces memory usage when accessibility is enabled ([#2678](https://github.com/emilk/egui/pull/2678)).
* Improve `DragValue` behavior ([#2649](https://github.com/emilk/egui/pull/2649), [#2650](https://github.com/emilk/egui/pull/2650), [#2688](https://github.com/emilk/egui/pull/2688), [#2638](https://github.com/emilk/egui/pull/2638)).
### Fixed 🐛 ### Fixed 🐛
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)). * Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
* Menus are now moved to fit on the screen.
* Fix `Window::pivot` causing windows to move around ([#2694](https://github.com/emilk/egui/pull/2694)).
## 0.20.1 - 2022-12-11 - Fix key-repeat ## 0.20.1 - 2022-12-11 - Fix key-repeat

1849
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ members = [
"crates/egui_demo_app", "crates/egui_demo_app",
"crates/egui_demo_lib", "crates/egui_demo_lib",
"crates/egui_extras", "crates/egui_extras",
"crates/egui_glium",
"crates/egui_glow", "crates/egui_glow",
"crates/egui-wgpu", "crates/egui-wgpu",
"crates/egui-winit", "crates/egui-winit",
@ -26,8 +27,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):

View file

@ -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,13 @@ 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). * [`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!
@ -372,8 +371,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 +396,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).

View file

@ -3,9 +3,6 @@ All notable changes to the `ecolor` crate will be noted in this file.
## Unreleased ## Unreleased
## 0.21.0 - 2023-02-08
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)). * Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ecolor" name = "ecolor"
version = "0.21.0" version = "0.20.0"
authors = [ authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>", "Andreas Reich <reichandreas@gmx.de>",

View file

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

View file

@ -198,19 +198,4 @@ impl Color32 {
// we need a somewhat expensive conversion to linear space and back. // we need a somewhat expensive conversion to linear space and back.
Rgba::from(self).multiply(factor).into() Rgba::from(self).multiply(factor).into()
} }
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
///
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
/// in order to obtain linear space color values.
#[inline]
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
let Self([r, g, b, a]) = self;
[
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
]
}
} }

View file

@ -6,37 +6,12 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
## Unreleased ## Unreleased
## 0.21.3 - 2023-02-15
* Fix typing the letter 'P' on web ([#2740](https://github.com/emilk/egui/pull/2740)).
## 0.21.2 - 2023-02-12
* Allow compiling `eframe` with `--no-default-features` ([#2728](https://github.com/emilk/egui/pull/2728)).
## 0.21.1 - 2023-02-12
* Fixed crash when native window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
## 0.21.0 - 2023-02-08 - Update to `winit` 0.28
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
#### Desktop/Native: #### Desktop/Native:
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)). * `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Fix bug where the cursor could get stuck using the wrong icon.
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
#### Web:
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
## 0.20.1 - 2022-12-11 ## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)). * Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support ## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support

View file

@ -1,6 +1,6 @@
[package] [package]
name = "eframe" name = "eframe"
version = "0.21.3" version = "0.20.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively" description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021" edition = "2021"
@ -14,7 +14,9 @@ keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true # Avoid speech-dispatcher dependencies - see https://docs.rs/crate/eframe/0.20.0/builds/695200
no-default-features = true
features = ["document-features", "glow", "wgpu", "persistence", "wgpu"]
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[lib] [lib]
@ -36,7 +38,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", "dep:egui_glow", "dep:glutin"]
## Enable saving app state to disk. ## Enable saving app state to disk.
persistence = [ persistence = [
@ -53,10 +55,8 @@ 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. ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
## This is used to generate images for the examples. ## This is used to generate images for the examples.
@ -68,7 +68,7 @@ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
[dependencies] [dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [ egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
"bytemuck", "bytemuck",
"tracing", "tracing",
] } ] }
@ -79,38 +79,44 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false } egui_glow = { version = "0.20.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.20.0", path = "../egui-winit", default-features = false, features = [
"clipboard", "clipboard",
"links", "links",
] } ] }
raw-window-handle = { version = "0.5.0" } raw-window-handle = { version = "0.5.0" }
winit = "0.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.20.0", path = "../egui-wgpu", optional = true, features = [
"winit", "winit",
] } # if wgpu is used, use it with winit ] } # if wgpu is used, use it with winit
pollster = { version = "0.3", optional = true } # needed for wgpu pollster = { version = "0.2", optional = true } # needed for wgpu
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps. # we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
# this can be done at the same time we expose x11/wayland features of winit crate. # this can be done at the same time we expose x11/wayland features of winit crate.
glutin = { version = "0.30", optional = true } glutin = { version = "0.30.0", optional = true, es = [
glutin-winit = { version = "0.3.0", optional = true } "egl",
"glx",
"x11",
"wayland",
"wgl",
] }
image = { version = "0.24", optional = true, default-features = false, features = [ image = { version = "0.24", optional = true, default-features = false, features = [
"png", "png",
] } ] }
puffin = { version = "0.14", optional = true } puffin = { version = "0.14", optional = true }
wgpu = { version = "0.15.0", optional = true } wgpu = { version = "0.14", optional = true }
# ------------------------------------------- # -------------------------------------------
# web: # web:
@ -118,7 +124,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 +170,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.20.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"] }

View file

@ -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.

View file

@ -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.
@ -152,21 +149,13 @@ pub trait App {
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 +173,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.
/// ///
@ -320,7 +309,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 +347,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 +373,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 +389,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 +405,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 +429,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 +550,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 +563,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 +578,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 +590,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;
@ -741,18 +709,6 @@ impl Frame {
self.output.close = true; self.output.close = true;
} }
/// Minimize or unminimize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_minimized(&mut self, minimized: bool) {
self.output.minimized = Some(minimized);
}
/// Maximize or unmaximize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_maximized(&mut self, maximized: bool) {
self.output.maximized = Some(maximized);
}
/// Tell `eframe` to close the desktop window. /// Tell `eframe` to close the desktop window.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[deprecated = "Renamed `close`"] #[deprecated = "Renamed `close`"]
@ -833,8 +789,13 @@ impl Frame {
} }
} }
/// Minimize or un-minimize window
#[cfg(not(target_arch = "wasm32"))]
pub fn set_minimized(&mut self, minimized: bool) {
self.output.minimized = Some(minimized);
}
/// 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 +826,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,
@ -1054,12 +1009,8 @@ pub(crate) mod backend {
#[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. /// Set to some bool to change window minimization
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub minimized: Option<bool>, pub minimized: Option<bool>,
/// Set to some bool to maximize or unmaximize window.
#[cfg(not(target_arch = "wasm32"))]
pub maximized: Option<bool>,
} }
} }

View file

@ -137,7 +137,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.
@ -180,7 +179,6 @@ mod native;
/// This function can fail if we fail to set up a graphics context. /// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub fn run_native( pub fn run_native(
app_name: &str, app_name: &str,
native_options: NativeOptions, native_options: NativeOptions,
@ -223,40 +221,36 @@ pub enum Error {
Glutin(#[from] glutin::error::Error), Glutin(#[from] glutin::error::Error),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))] #[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")] #[error("Found no glutin configs matching the template: {0:?}")]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>), NoGlutinConfigs(glutin::config::ConfigTemplate),
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
#[error("WGPU error: {0}")] #[error("WGPU error: {0}")]
Wgpu(#[from] egui_wgpu::WgpuError), Wgpu(#[from] wgpu::RequestDeviceError),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))] macro_rules! profile_function {
mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => { ($($arg: tt)*) => {
#[cfg(feature = "puffin")] #[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*); puffin::profile_function!($($arg)*);
}; };
} }
pub(crate) use profile_function; #[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin" /// Profiling macro for feature "puffin"
macro_rules! profile_scope { #[cfg(not(target_arch = "wasm32"))]
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::*;

View file

@ -12,14 +12,6 @@ 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 +19,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 +38,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,
@ -72,6 +56,7 @@ pub fn window_builder<E>(
window_settings: Option<WindowSettings>, window_settings: Option<WindowSettings>,
) -> winit::window::WindowBuilder { ) -> winit::window::WindowBuilder {
let epi::NativeOptions { let epi::NativeOptions {
always_on_top,
maximized, maximized,
decorated, decorated,
fullscreen, fullscreen,
@ -85,7 +70,6 @@ pub fn window_builder<E>(
max_window_size, max_window_size,
resizable, resizable,
transparent, transparent,
centered,
.. ..
} = native_options; } = native_options;
@ -93,6 +77,7 @@ pub fn window_builder<E>(
let mut window_builder = winit::window::WindowBuilder::new() let mut window_builder = winit::window::WindowBuilder::new()
.with_title(title) .with_title(title)
.with_always_on_top(*always_on_top)
.with_decorations(*decorated) .with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))) .with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
.with_maximized(*maximized) .with_maximized(*maximized)
@ -120,16 +105,13 @@ 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(mut window_settings) = window_settings {
// Restore pos/size from previous session // Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop)); window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)]
window_settings.clamp_window_to_sane_position(&event_loop);
window_builder = window_settings.initialize_window(window_builder); window_builder = window_settings.initialize_window(window_builder);
window_settings.inner_size_points()
} else { } else {
if let Some(pos) = *initial_window_pos { if let Some(pos) = *initial_window_pos {
window_builder = window_builder.with_position(winit::dpi::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,
}); });
@ -140,37 +122,9 @@ pub fn window_builder<E>(
initial_window_size.at_most(largest_monitor_point_size(event_loop)); initial_window_size.at_most(largest_monitor_point_size(event_loop));
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size)); window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
} }
*initial_window_size
};
if *centered {
if let Some(monitor) = event_loop.available_monitors().next() {
let monitor_size = monitor.size();
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0 && monitor_size.height > 0 {
let x = (monitor_size.width - inner_size.x as u32) / 2;
let y = (monitor_size.height - inner_size.y as u32) / 2;
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
x: x as f64,
y: y as f64,
});
}
}
} }
window_builder
}
pub fn apply_native_options_to_window( window_builder
window: &winit::window::Window,
native_options: &crate::NativeOptions,
) {
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
} }
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 { fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
@ -215,7 +169,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: _,
@ -228,7 +181,6 @@ pub fn handle_app_output(
visible: _, // handled in post_present visible: _, // handled in post_present
always_on_top, always_on_top,
minimized, minimized,
maximized,
} = app_output; } = app_output;
if let Some(decorated) = decorated { if let Some(decorated) = decorated {
@ -265,22 +217,11 @@ 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 { if let Some(minimized) = minimized {
window.set_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 +248,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 +262,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,7 +296,6 @@ 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,
} }
} }
@ -381,18 +314,17 @@ impl EpiIntegration {
egui_ctx.enable_accesskit(); egui_ctx.enable_accesskit();
// Enqueue a repaint so we'll receive a full tree update soon. // Enqueue a repaint so we'll receive a full tree update soon.
egui_ctx.request_repaint(); egui_ctx.request_repaint();
egui_ctx.accesskit_placeholder_tree_update() egui::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();
} }
@ -444,16 +376,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);
@ -466,12 +394,7 @@ impl EpiIntegration {
tracing::debug!("App::on_close_event returned {}", self.close); tracing::debug!("App::on_close_event returned {}", self.close);
} }
self.frame.output.visible = app_output.visible; // this is handled by post_present self.frame.output.visible = app_output.visible; // this is handled by post_present
handle_app_output( 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 +451,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");

View file

@ -38,7 +38,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,28 +45,20 @@ 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>,
@ -161,8 +152,8 @@ fn run_and_return(
event => match winit_app.on_event(event_loop, event) { event => match winit_app.on_event(event_loop, event) {
Ok(event_result) => event_result, Ok(event_result) => event_result,
Err(err) => { Err(err) => {
tracing::error!("Exiting because of error: {err:?} on event {event:?}");
returned_result = Err(err); returned_result = Err(err);
tracing::debug!("Exiting because of an error");
EventResult::Exit EventResult::Exit
} }
}, },
@ -299,6 +290,27 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
}) })
} }
fn center_window_pos(
monitor: Option<winit::monitor::MonitorHandle>,
native_options: &mut epi::NativeOptions,
) {
// Get the current_monitor.
if let Some(monitor) = monitor {
let monitor_size = monitor.size();
let inner_size = native_options
.initial_window_size
.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0 && monitor_size.height > 0 {
let x = (monitor_size.width - inner_size.x as u32) / 2;
let y = (monitor_size.height - inner_size.y as u32) / 2;
native_options.initial_window_pos = Some(egui::Pos2 {
x: x as _,
y: y as _,
});
}
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Run an egui app /// Run an egui app
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@ -306,12 +318,6 @@ mod glow_integration {
use std::sync::Arc; use std::sync::Arc;
use egui::NumExt as _; use egui::NumExt as _;
use glutin::{
display::GetGlDisplay,
prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext},
surface::GlSurface,
};
use raw_window_handle::HasRawWindowHandle;
use super::*; use super::*;
@ -336,260 +342,132 @@ mod glow_integration {
painter: egui_glow::Painter, painter: egui_glow::Painter,
integration: epi_integration::EpiIntegration, integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>, app: Box<dyn epi::App>,
// Conceptually this will be split out eventually so that the rest of the state // Conceptually this will be split out eventually so that the rest of the state
// can be persistent. // can be persistent.
gl_window: GlutinWindowContext, gl_window: GlutinWindowContext,
} }
/// This struct will contain both persistent and temporary glutin state.
///
/// Platform Quirks:
/// * Microsoft Windows: requires that we create a window before opengl context.
/// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event.
///
/// winit guarantees that we will get a Resumed event on startup on all platforms.
/// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`.
/// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android.
/// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event.
///
/// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of
/// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended.
struct GlutinWindowContext { struct GlutinWindowContext {
builder: winit::window::WindowBuilder, window: winit::window::Window,
swap_interval: glutin::surface::SwapInterval, gl_context: glutin::context::PossiblyCurrentContext,
gl_config: glutin::config::Config, gl_display: glutin::display::Display,
current_gl_context: Option<glutin::context::PossiblyCurrentContext>, gl_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
window: Option<winit::window::Window>,
} }
impl GlutinWindowContext { impl GlutinWindowContext {
/// There is a lot of complexity with opengl creation, so prefer extensivve logging to get all the help we can to debug issues. // refactor this function to use `glutin-winit` crate eventually.
/// // preferably add android support at the same time.
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn new( unsafe fn new(
winit_window_builder: winit::window::WindowBuilder, winit_window: winit::window::Window,
native_options: &epi::NativeOptions, native_options: &epi::NativeOptions,
event_loop: &EventLoopWindowTarget<UserEvent>,
) -> Result<Self> { ) -> Result<Self> {
use glutin::prelude::*; use glutin::prelude::*;
// convert native options to glutin options use raw_window_handle::*;
let hardware_acceleration = match native_options.hardware_acceleration { let hardware_acceleration = match native_options.hardware_acceleration {
crate::HardwareAcceleration::Required => Some(true), crate::HardwareAcceleration::Required => Some(true),
crate::HardwareAcceleration::Preferred => None, crate::HardwareAcceleration::Preferred => None,
crate::HardwareAcceleration::Off => Some(false), crate::HardwareAcceleration::Off => Some(false),
}; };
let raw_display_handle = winit_window.raw_display_handle();
let raw_window_handle = winit_window.raw_window_handle();
// EGL is crossplatform and the official khronos way
// but sometimes platforms/drivers may not have it, so we use back up options where possible.
// TODO: check whether we can expose these options as "features", so that users can select the relevant backend they want.
// try egl and fallback to windows wgl. Windows is the only platform that *requires* window handle to create display.
#[cfg(target_os = "windows")]
let preference =
glutin::display::DisplayApiPreference::EglThenWgl(Some(raw_window_handle));
// try egl and fallback to x11 glx
#[cfg(target_os = "linux")]
let preference = glutin::display::DisplayApiPreference::EglThenGlx(Box::new(
winit::platform::unix::register_xlib_error_hook,
));
#[cfg(target_os = "macos")]
let preference = glutin::display::DisplayApiPreference::Cgl;
#[cfg(target_os = "android")]
let preference = glutin::display::DisplayApiPreference::Egl;
let gl_display = glutin::display::Display::new(raw_display_handle, preference)?;
let swap_interval = if native_options.vsync { let swap_interval = if native_options.vsync {
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
} else { } else {
glutin::surface::SwapInterval::DontWait glutin::surface::SwapInterval::DontWait
}; };
/* opengl setup flow goes like this:
1. we create a configuration for opengl "Display" / "Config" creation let config_template = glutin::config::ConfigTemplateBuilder::new()
2. choose between special extensions like glx or egl or wgl and use them to create config/display
3. opengl context configuration
4. opengl context creation
*/
// start building config for gl display
let config_template_builder = glutin::config::ConfigTemplateBuilder::new()
.prefer_hardware_accelerated(hardware_acceleration) .prefer_hardware_accelerated(hardware_acceleration)
.with_depth_size(native_options.depth_buffer) .with_depth_size(native_options.depth_buffer);
.with_stencil_size(native_options.stencil_buffer)
.with_transparency(native_options.transparent);
// we don't know if multi sampling option is set. so, check if its more than 0. // we don't know if multi sampling option is set. so, check if its more than 0.
let config_template_builder = if native_options.multisampling > 0 { let config_template = if native_options.multisampling > 0 {
config_template_builder.with_multisampling( config_template.with_multisampling(
native_options native_options
.multisampling .multisampling
.try_into() .try_into()
.expect("failed to fit multisamples option of native_options into u8"), .expect("failed to fit multisamples into u8"),
) )
} else { } else {
config_template_builder config_template
}; };
let config_template = config_template
.with_stencil_size(native_options.stencil_buffer)
.with_transparency(native_options.transparent)
.compatible_with_native_window(raw_window_handle)
.build();
// finds all valid configurations supported by this display that match the config_template
// this is where we will try to get a "fallback" config if we are okay with ignoring some native
// options required by user like multi sampling, srgb, transparency etc..
// TODO: need to figure out a good fallback config template
let config = gl_display
.find_configs(config_template.clone())?
.next()
.ok_or(crate::Error::NoGlutinConfigs(config_template))?;
tracing::debug!(
"trying to create glutin Display with config: {:?}",
&config_template_builder
);
// create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android.
let (window, gl_config) = glutin_winit::DisplayBuilder::new()
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
.with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
.with_window_builder(Some(winit_window_builder.clone()))
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
"failed to find a matching configuration for creating glutin config",
);
tracing::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
let gl_display = gl_config.display();
tracing::debug!(
"successfully created GL Display with version: {} and supported features: {:?}",
gl_display.version_string(),
gl_display.supported_features()
);
let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle());
tracing::debug!(
"creating gl context using raw window handle: {:?}",
raw_window_handle
);
// create gl context. if core context cannot be created, try gl es context as fallback.
let context_attributes = let context_attributes =
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle); glutin::context::ContextAttributesBuilder::new().build(Some(raw_window_handle));
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() // for surface creation.
.with_context_api(glutin::context::ContextApi::Gles(None)) let (width, height): (u32, u32) = winit_window.inner_size().into();
.build(raw_window_handle);
let gl_context = match gl_config
.display()
.create_context(&gl_config, &context_attributes)
{
Ok(it) => it,
Err(err) => {
tracing::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}");
tracing::debug!("retrying with fallback context attributes: {fallback_context_attributes:?}");
gl_config
.display()
.create_context(&gl_config, &fallback_context_attributes)?
}
};
let not_current_gl_context = Some(gl_context);
// the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but
// it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might
// help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc..
// https://github.com/emilk/egui/pull/2541#issuecomment-1370767582
Ok(GlutinWindowContext {
builder: winit_window_builder,
swap_interval,
gl_config,
current_gl_context: None,
window,
gl_surface: None,
not_current_gl_context,
})
}
/// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime.
/// roughly,
/// 1. check if window already exists. otherwise, create one now.
/// 2. create attributes for surface creation.
/// 3. create surface.
/// 4. make surface and context current.
///
/// we presently assume that we will
#[allow(unsafe_code)]
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
if self.gl_surface.is_some() {
tracing::warn!(
"on_resume called even thought we already have a surface. early return"
);
return Ok(());
}
tracing::debug!("running on_resume fn.");
// make sure we have a window or create one.
let window = self.window.take().unwrap_or_else(|| {
tracing::debug!("window doesn't exist yet. creating one now with finalize_window");
glutin_winit::finalize_window(event_loop, self.builder.clone(), &self.gl_config)
.expect("failed to finalize glutin window")
});
// surface attributes
let (width, height): (u32, u32) = window.inner_size().into();
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap(); let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap(); let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
let surface_attributes = let surface_attributes =
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new() glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(window.raw_window_handle(), width, height); .build(raw_window_handle, width, height);
tracing::debug!( // start creating the gl objects
"creating surface with attributes: {:?}", let gl_context = gl_display.create_context(&config, &context_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(&current_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 let gl_surface = gl_display.create_window_surface(&config, &surface_attributes)?;
fn on_suspend(&mut self) -> Result<()> { let gl_context = gl_context.make_current(&gl_surface)?;
tracing::debug!("received suspend event. dropping window and surface"); gl_surface.set_swap_interval(&gl_context, swap_interval)?;
self.gl_surface.take(); Ok(GlutinWindowContext {
self.window.take(); window: winit_window,
if let Some(current) = self.current_gl_context.take() { gl_context,
tracing::debug!("context is current, so making it non-current"); gl_display,
self.not_current_gl_context = Some(current.make_not_current()?); gl_surface,
} else { })
tracing::debug!(
"context is already not current??? could be duplicate suspend event"
);
}
Ok(())
} }
fn window(&self) -> &winit::window::Window { fn window(&self) -> &winit::window::Window {
self.window.as_ref().expect("winit window doesn't exist") &self.window
} }
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) { fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
use glutin::surface::GlSurface;
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
self.gl_surface self.gl_surface.resize(&self.gl_context, width, height);
.as_ref()
.expect("failed to get surface to resize")
.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width,
height,
);
} }
fn swap_buffers(&self) -> glutin::error::Result<()> { fn swap_buffers(&self) -> glutin::error::Result<()> {
self.gl_surface use glutin::surface::GlSurface;
.as_ref() self.gl_surface.swap_buffers(&self.gl_context)
.expect("failed to get surface to swap buffers")
.swap_buffers(
self.current_gl_context
.as_ref()
.expect("failed to get current context to swap buffers"),
)
} }
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
self.gl_config.display().get_proc_address(addr) use glutin::display::GlDisplay;
self.gl_display.get_proc_address(addr)
} }
} }
@ -637,17 +515,12 @@ mod glow_integration {
let window_settings = epi_integration::load_window_settings(storage); let window_settings = epi_integration::load_window_settings(storage);
let winit_window_builder = let winit_window =
epi_integration::window_builder(event_loop, title, native_options, window_settings); epi_integration::window_builder(event_loop, title, native_options, window_settings)
let mut glutin_window_context = unsafe { .build(event_loop)?;
GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? // a lot of the code below has been lifted from glutin example in their repo.
}; let glutin_window_context =
glutin_window_context.on_resume(event_loop)?; unsafe { GlutinWindowContext::new(winit_window, native_options)? };
if let Some(window) = &glutin_window_context.window {
epi_integration::apply_native_options_to_window(window, native_options);
}
let gl = unsafe { let gl = unsafe {
glow::Context::from_loader_function(|s| { glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s) let s = std::ffi::CString::new(s)
@ -876,24 +749,26 @@ mod glow_integration {
) -> Result<EventResult> { ) -> Result<EventResult> {
Ok(match event { Ok(match event {
winit::event::Event::Resumed => { winit::event::Event::Resumed => {
// first resume event.
// we can actually move this outside of event loop.
// and just run the on_resume fn of gl_window
if self.running.is_none() { if self.running.is_none() {
self.init_run_state(event_loop)?; self.init_run_state(event_loop)?;
} else {
// not the first resume event. create whatever you need.
self.running
.as_mut()
.unwrap()
.gl_window
.on_resume(event_loop)?;
} }
EventResult::RepaintNow EventResult::RepaintNow
} }
winit::event::Event::Suspended => { winit::event::Event::Suspended => {
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
} }
@ -988,13 +863,22 @@ mod glow_integration {
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
) -> Result<()> { ) -> Result<()> {
if native_options.run_and_return { if native_options.run_and_return {
with_event_loop(native_options, |event_loop, native_options| { with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let glow_eframe = let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, app_creator); GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, glow_eframe) run_and_return(event_loop, glow_eframe)
}) })
} else { } else {
let event_loop = create_event_loop_builder(&mut native_options).build(); let event_loop = create_event_loop_builder(&mut native_options).build();
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe); run_and_exit(event_loop, glow_eframe);
} }
@ -1062,20 +946,19 @@ mod wgpu_integration {
storage: Option<&dyn epi::Storage>, storage: Option<&dyn epi::Storage>,
title: &str, title: &str,
native_options: &NativeOptions, native_options: &NativeOptions,
) -> std::result::Result<winit::window::Window, winit::error::OsError> { ) -> Result<winit::window::Window> {
let window_settings = epi_integration::load_window_settings(storage); let window_settings = epi_integration::load_window_settings(storage);
let window_builder = Ok(
epi_integration::window_builder(event_loop, title, native_options, window_settings); epi_integration::window_builder(event_loop, title, native_options, window_settings)
let window = window_builder.build(event_loop)?; .build(event_loop)?,
epi_integration::apply_native_options_to_window(&window, native_options); )
Ok(window)
} }
#[allow(unsafe_code)] #[allow(unsafe_code)]
fn set_window( fn set_window(
&mut self, &mut self,
window: winit::window::Window, window: winit::window::Window,
) -> std::result::Result<(), egui_wgpu::WgpuError> { ) -> std::result::Result<(), wgpu::RequestDeviceError> {
self.window = Some(window); self.window = Some(window);
if let Some(running) = &mut self.running { if let Some(running) = &mut self.running {
unsafe { unsafe {
@ -1087,7 +970,7 @@ mod wgpu_integration {
#[allow(unsafe_code)] #[allow(unsafe_code)]
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> { fn drop_window(&mut self) -> std::result::Result<(), wgpu::RequestDeviceError> {
self.window = None; self.window = None;
if let Some(running) = &mut self.running { if let Some(running) = &mut self.running {
unsafe { unsafe {
@ -1102,14 +985,13 @@ 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> { ) -> std::result::Result<(), wgpu::RequestDeviceError> {
#[allow(unsafe_code, unused_mut, unused_unsafe)] #[allow(unsafe_code, unused_mut, unused_unsafe)]
let painter = unsafe { let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new( let mut painter = egui_wgpu::winit::Painter::new(
self.native_options.wgpu_options.clone(), self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _, self.native_options.multisampling.max(1) as _,
self.native_options.depth_buffer, self.native_options.depth_buffer,
self.native_options.transparent,
); );
pollster::block_on(painter.set_window(Some(&window)))?; pollster::block_on(painter.set_window(Some(&window)))?;
painter painter
@ -1404,13 +1286,22 @@ mod wgpu_integration {
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
) -> Result<()> { ) -> Result<()> {
if native_options.run_and_return { if native_options.run_and_return {
with_event_loop(native_options, |event_loop, native_options| { with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let wgpu_eframe = let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, wgpu_eframe) run_and_return(event_loop, wgpu_eframe)
}) })
} else { } else {
let event_loop = create_event_loop_builder(&mut native_options).build(); let event_loop = create_event_loop_builder(&mut native_options).build();
if native_options.centered {
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
}
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe); run_and_exit(event_loop, wgpu_eframe);
} }

View file

@ -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());
} }
@ -450,6 +449,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 +467,6 @@ impl EventToUnsubscribe {
} }
} }
} }
pub struct AppRunnerContainer { pub struct AppRunnerContainer {
pub runner: AppRunnerRef, pub runner: AppRunnerRef,
@ -485,6 +485,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

View file

@ -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,9 +64,8 @@ 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,
@ -88,18 +85,10 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input(); 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 +112,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();
} }
}, },
)?; )?;
@ -210,21 +198,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 +390,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

View file

@ -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()

View file

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

View file

@ -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()?;

View file

@ -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,

View file

@ -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,

View file

@ -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,
}, },

View file

@ -3,14 +3,9 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.21.0 - 2023-02-08
* Update to `wgpu` 0.15 ([#2629](https://github.com/emilk/egui/pull/2629))
* Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)). * Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)).
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)). * `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)). * `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
* `winit::Painter` now supports transparent backbuffer ([#2684](https://github.com/emilk/egui/pull/2684)).
## 0.20.0 - 2022-12-08 - web support ## 0.20.0 - 2022-12-08 - web support

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui-wgpu" name = "egui-wgpu"
version = "0.21.0" version = "0.20.0"
description = "Bindings for using egui natively using the wgpu library" description = "Bindings for using egui natively using the wgpu library"
authors = [ authors = [
"Nils Hasenbanck <nils@hasenbanck.de>", "Nils Hasenbanck <nils@hasenbanck.de>",
@ -36,20 +36,20 @@ winit = ["dep:winit"]
[dependencies] [dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [ epaint = { version = "0.20.0", path = "../epaint", default-features = false, features = [
"bytemuck", "bytemuck",
] } ] }
bytemuck = "1.7" bytemuck = "1.7"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
type-map = "0.5.0" type-map = "0.5.0"
wgpu = "0.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 } winit = { version = "0.27.2", optional = true }
# Native: # Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View file

@ -99,39 +99,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"

View file

@ -554,7 +554,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

View file

@ -1,14 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use epaint::mutex::RwLock;
use tracing::error; use tracing::error;
use wgpu::{Adapter, Instance, Surface};
use epaint::mutex::RwLock;
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 +19,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 +41,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,
@ -76,7 +66,7 @@ impl Painter {
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> { ) -> Result<RenderState, wgpu::RequestDeviceError> {
adapter adapter
@ -101,7 +91,7 @@ impl Painter {
// will have the same format and so this render state will remain valid. // will have the same format and so this render state will remain valid.
async fn ensure_render_state_for_surface( async fn ensure_render_state_for_surface(
&mut self, &mut self,
surface: &wgpu::Surface, surface: &Surface,
) -> Result<(), wgpu::RequestDeviceError> { ) -> Result<(), wgpu::RequestDeviceError> {
if self.adapter.is_none() { if self.adapter.is_none() {
self.adapter = self self.adapter = self
@ -117,7 +107,7 @@ impl Painter {
match &self.adapter { match &self.adapter {
Some(adapter) => { Some(adapter) => {
let swapchain_format = crate::preferred_framebuffer_format( let swapchain_format = crate::preferred_framebuffer_format(
&surface.get_capabilities(adapter).formats, &surface.get_supported_formats(adapter),
); );
let rs = self.init_render_state(adapter, swapchain_format).await?; let rs = self.init_render_state(adapter, swapchain_format).await?;
self.render_state = Some(rs); self.render_state = Some(rs);
@ -128,23 +118,33 @@ impl Painter {
Ok(()) 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;
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format, let config = wgpu::SurfaceConfiguration {
width: surface_state.width, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
height: surface_state.height, format,
present_mode, width: width_in_pixels,
alpha_mode: surface_state.alpha_mode, height: height_in_pixels,
view_formats: vec![render_state.target_format], present_mode: self.configuration.present_mode,
}, alpha_mode: wgpu::CompositeAlphaMode::Auto,
); };
let surface_state = self
.surface_state
.as_mut()
.expect("Surface state should exist before surface configuration");
surface_state
.surface
.configure(&render_state.device, &config);
surface_state.width = width_in_pixels;
surface_state.height = height_in_pixels;
} }
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
@ -177,41 +177,22 @@ impl Painter {
pub async unsafe fn set_window( pub async unsafe fn set_window(
&mut self, &mut self,
window: Option<&winit::window::Window>, window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> { ) -> Result<(), wgpu::RequestDeviceError> {
match window { match window {
Some(window) => { Some(window) => {
let surface = self.instance.create_surface(&window)?; let surface = self.instance.create_surface(&window);
self.ensure_render_state_for_surface(&surface).await?; self.ensure_render_state_for_surface(&surface).await?;
let alpha_mode = if self.support_transparent_backbuffer {
let supported_alpha_modes = surface
.get_capabilities(self.adapter.as_ref().unwrap())
.alpha_modes;
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else if supported_alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else {
tracing::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
wgpu::CompositeAlphaMode::Auto
}
} else {
wgpu::CompositeAlphaMode::Auto
};
let size = window.inner_size(); let size = window.inner_size();
let width = size.width;
let height = size.height;
self.surface_state = Some(SurfaceState { self.surface_state = Some(SurfaceState {
surface, surface,
width: 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;
@ -236,17 +217,10 @@ impl Painter {
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 +234,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,7 +250,7 @@ impl Painter {
pub fn paint_and_update_textures( pub fn paint_and_update_textures(
&mut self, &mut self,
pixels_per_point: f32, pixels_per_point: f32,
clear_color: [f32; 4], clear_color: epaint::Rgba,
clipped_primitives: &[epaint::ClippedPrimitive], clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta, textures_delta: &epaint::textures::TexturesDelta,
) { ) {
@ -291,6 +264,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 +277,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 +295,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 +330,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,
}, },

View file

@ -5,19 +5,8 @@ All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased ## Unreleased
## 0.21.1 - 2023-02-12
* Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)).
## 0.21.0 - 2023-02-08
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
* Fix bug where the cursor could get stuck using the wrong icon.
## 0.20.1 - 2022-12-11 ## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)). * Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08 ## 0.20.0 - 2022-12-08

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui-winit" name = "egui-winit"
version = "0.21.1" version = "0.20.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit" description = "Bindings for using egui with winit"
edition = "2021" edition = "2021"
@ -14,7 +14,8 @@ keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true # Avoid speech-dispatcher dependencies - see https://docs.rs/crate/egui-winit/0.20.0/builds/695196
features = ["document-features"]
[features] [features]
@ -36,6 +37,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 +47,33 @@ 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.20.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 }
# feature accesskit
accesskit_winit = { version = "0.7.1", optional = true }
puffin = { version = "0.14", optional = true } puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] } serde = { version = "1.0", optional = true, features = ["derive"] }
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"] }

View file

@ -19,12 +19,23 @@ 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 +70,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.
@ -100,10 +111,11 @@ 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,
@ -368,9 +380,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,26 +391,10 @@ 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(),
}
}
} }
} }
@ -620,6 +615,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,
@ -655,25 +655,22 @@ impl State {
} }
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) { fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
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. // On other platforms: just early-out to save CPU.
// On other platforms: just early-out to save CPU. if self.current_cursor_icon == cursor_icon {
return; return;
} }
self.current_cursor_icon = cursor_icon;
let is_pointer_in_window = self.pointer_pos_in_points.is_some(); if let Some(cursor_icon) = translate_cursor(cursor_icon) {
if is_pointer_in_window { window.set_cursor_visible(true);
self.current_cursor_icon = Some(cursor_icon);
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) { let is_pointer_in_window = self.pointer_pos_in_points.is_some();
window.set_cursor_visible(true); if is_pointer_in_window {
window.set_cursor_icon(winit_cursor_icon); window.set_cursor_icon(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 +879,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();
} }

View file

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

View file

@ -42,10 +42,6 @@ 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,
@ -53,13 +49,16 @@ impl WindowSettings {
// If the app last ran on two monitors and only one is now connected, then // If the app last ran on two monitors and only one is now connected, then
// the given position is invalid. // the given position is invalid.
// If this happens on Mac, the window is clamped into valid area. // If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the 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 { let try_restore_position = !cfg!(target_os = "windows");
window = window.with_position(winit::dpi::PhysicalPosition { if try_restore_position {
x: pos.x as f64, if let Some(pos) = self.position {
y: pos.y as f64, window = window.with_position(winit::dpi::PhysicalPosition {
}); x: pos.x as f64,
y: pos.y as f64,
});
}
} }
if let Some(inner_size_points) = self.inner_size_points { if let Some(inner_size_points) = self.inner_size_points {
@ -87,58 +86,4 @@ impl WindowSettings {
*size = size.at_most(max_size); *size = size.at_most(max_size);
} }
} }
pub fn clamp_window_to_sane_position<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
}
}
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui" name = "egui"
version = "0.21.0" version = "0.20.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native" description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021" edition = "2021"
@ -54,12 +54,8 @@ persistence = ["serde", "epaint/serde", "ron"]
## Allow serialization using [`serde`](https://docs.rs/serde). ## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"] serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
[dependencies] [dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false } epaint = { version = "0.20.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
@ -70,7 +66,7 @@ nohash-hasher = "0.2"
#! ### Optional dependencies #! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform ## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration. ## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.9.0", optional = true } accesskit = { version = "0.8.1", 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 }

View file

@ -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,
} }

View file

@ -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)
} }
} }
@ -247,25 +231,27 @@ 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).copied();
let is_new = state.is_none(); let is_new = state.is_none();
if is_new { if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
} }
let mut state = state.unwrap_or_else(|| State { let mut state = state.unwrap_or_else(|| State {
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;
if pivot != Align2::LEFT_TOP {
state.pos.x -= pivot.x().to_factor() * state.size.x;
state.pos.y -= pivot.y().to_factor() * state.size.y;
}
if let Some((anchor, offset)) = anchor { if let Some((anchor, offset)) = anchor {
let screen = ctx.available_rect(); let screen = ctx.available_rect();
state.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 +278,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 {
@ -345,7 +329,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 +358,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 +374,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 +410,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 +418,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 +426,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()
.visible_windows() .areas
.into_iter() .visible_windows()
.map(State::rect) .into_iter()
.collect() .map(State::rect)
}); .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();

View file

@ -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 {
@ -554,7 +555,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 +575,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 {

View file

@ -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,17 +240,21 @@ 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 {
@ -257,20 +263,18 @@ fn combo_box_dyn<'c, R>(
let margin = ui.spacing().button_padding; let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
let icon_spacing = ui.spacing().icon_spacing;
// We don't want to change width when user selects something new // We don't want to change width when user selects something new
let full_minimum_width = if wrap_enabled { let full_minimum_width = if wrap_enabled {
// Currently selected value's text will be wrapped if needed, so occupy the available width. // Currently selected value's text will be wrapped if needed, so occupy the available width.
ui.available_width() ui.available_width()
} else { } else {
// Occupy at least the minimum width assigned to ComboBox. // Occupy at least the minimum width assigned to Slider and ComboBox.
let width = width.unwrap_or_else(|| ui.spacing().combo_width); ui.spacing().slider_width - 2.0 * margin.x
width - 2.0 * margin.x
}; };
let icon_size = Vec2::splat(ui.spacing().icon_width); let icon_size = Vec2::splat(ui.spacing().icon_width);
let wrap_width = if wrap_enabled { let wrap_width = if wrap_enabled {
// Use the available width, currently selected value's text will be wrapped if exceeds this value. // Use the available width, currently selected value's text will be wrapped if exceeds this value.
ui.available_width() - icon_spacing - icon_size.x ui.available_width() - ui.spacing().item_spacing.x - icon_size.x
} else { } else {
// Use all the width necessary to display the currently selected value's text. // Use all the width necessary to display the currently selected value's text.
f32::INFINITY f32::INFINITY
@ -284,7 +288,7 @@ fn combo_box_dyn<'c, R>(
full_minimum_width full_minimum_width
} else { } else {
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text. // Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
galley.size().x + icon_spacing + icon_size.x galley.size().x + ui.spacing().item_spacing.x + icon_size.x
}; };
// Case : wrap_enabled : occupy all the available width. // Case : wrap_enabled : occupy all the available width.
@ -329,7 +333,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 +390,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,
}, },
); );

View file

@ -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,
} }

View file

@ -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,7 +37,7 @@ 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);
} }
} }
@ -245,12 +245,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 +257,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,12 +296,12 @@ 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 { } else if show_separator_line {
// TOOD(emilk): distinguish resizable from non-resizable // TOOD(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke // dim ui.style().visuals.widgets.noninteractive.bg_stroke
} else { } else {
Stroke::NONE Stroke::NONE
}; };
@ -335,19 +334,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
} }
@ -683,7 +682,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 +695,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 +709,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,12 +748,12 @@ 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 { } else if show_separator_line {
// TOOD(emilk): distinguish resizable from non-resizable // TOOD(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke // dim ui.style().visuals.widgets.noninteractive.bg_stroke
} else { } else {
Stroke::NONE Stroke::NONE
}; };
@ -787,7 +787,7 @@ impl TopBottomPanel {
let available_rect = ctx.available_rect(); let available_rect = ctx.available_rect();
let side = self.side; let side = self.side;
let clip_rect = ctx.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 +795,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));
});
} }
} }
@ -1044,13 +1042,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
} }

View file

@ -13,11 +13,11 @@ pub(crate) struct TooltipState {
impl TooltipState { impl TooltipState {
pub fn load(ctx: &Context) -> Option<Self> { pub fn load(ctx: &Context) -> Option<Self> {
ctx.data_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> {
@ -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 {

View file

@ -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;
} }
} }

View file

@ -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);
} }
} }
@ -426,34 +426,15 @@ 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: // Nice handling of forced resizing beyond the possible:
let clip_rect_margin = ui.visuals().clip_rect_margin; for d in 0..2 {
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin; if !has_bar[d] {
let mut content_clip_rect = ui.clip_rect(); content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
for d in 0..2 {
if has_bar[d] {
if state.content_is_too_large[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
}
if state.show_scroll[d] {
// Make sure content doesn't cover scroll bars
let tiny_gap = 1.0;
content_clip_rect.max[1 - d] =
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
}
} else {
// Nice handling of forced resizing beyond the possible:
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
}
} }
// Make sure we din't accidentally expand the clip rect
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
content_ui.set_clip_rect(content_clip_rect);
} }
content_ui.set_clip_rect(content_clip_rect);
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
@ -468,10 +449,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 +459,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 +603,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 +668,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 +677,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 +819,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(),

View file

@ -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()) .min;
.left_top();
area.state_mut().set_left_top_pos(pos);
}
let full_response = area.end(ctx, area_content_ui); let full_response = area.end(ctx, area_content_ui);
@ -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

View file

@ -314,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters, /// as on mac that is how you type special characters,
/// so those key presses are usually not reported to egui. /// so those key presses are usually not reported to egui.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers { pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac). /// Either of the alt keys are down (option ⌥ on Mac).
@ -777,7 +777,7 @@ impl Key {
/// ///
/// Can be used with [`crate::InputState::consume_shortcut`] /// Can be used with [`crate::InputState::consume_shortcut`]
/// and [`crate::Context::format_shortcut`]. /// and [`crate::Context::format_shortcut`].
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyboardShortcut { pub struct KeyboardShortcut {
pub modifiers: Modifiers, pub modifiers: Modifiers,
pub key: Key, pub key: Key,

View file

@ -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();
/// } /// }
/// # }); /// # });
/// ``` /// ```

View file

@ -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 {
@ -12,7 +12,7 @@ pub(crate) struct TooltipFrameState {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct AccessKitFrameState { pub(crate) struct AccessKitFrameState {
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>, pub(crate) nodes: IdMap<Box<accesskit::Node>>,
pub(crate) parent_stack: Vec<Id>, pub(crate) parent_stack: Vec<Id>,
} }
@ -51,12 +51,6 @@ pub(crate) struct FrameState {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub(crate) accesskit_state: Option<AccessKitFrameState>, pub(crate) accesskit_state: Option<AccessKitFrameState>,
/// Highlight these widgets this next frame. Read from this.
pub(crate) highlight_this_frame: IdSet,
/// Highlight these widgets the next frame. Write to this.
pub(crate) highlight_next_frame: IdSet,
} }
impl Default for FrameState { impl Default for FrameState {
@ -71,8 +65,6 @@ impl Default for FrameState {
scroll_target: [None, None], scroll_target: [None, None],
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit_state: None, accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),
} }
} }
} }
@ -89,8 +81,6 @@ impl FrameState {
scroll_target, scroll_target,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit_state, accesskit_state,
highlight_this_frame,
highlight_next_frame,
} = self; } = self;
used_ids.clear(); used_ids.clear();
@ -100,13 +90,10 @@ impl FrameState {
*tooltip_state = None; *tooltip_state = None;
*scroll_delta = input.scroll_delta; *scroll_delta = input.scroll_delta;
*scroll_target = [None, None]; *scroll_target = [None, None];
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
{ {
*accesskit_state = None; *accesskit_state = None;
} }
*highlight_this_frame = std::mem::take(highlight_next_frame);
} }
/// How much space is still available after panels has been added. /// How much space is still available after panels has been added.

View file

@ -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) {

View file

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

View file

@ -168,8 +168,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>;

View file

@ -368,7 +368,7 @@ impl InputState {
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// let mut zoom = 1.0; // no zoom /// let mut zoom = 1.0; // no zoom
/// let mut rotation = 0.0; // no rotation /// let mut rotation = 0.0; // no rotation
/// let multi_touch = ui.input(|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;
@ -447,10 +447,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 +471,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 +480,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 +639,7 @@ impl PointerState {
Some(Click { Some(Click {
pos, pos,
button,
count, count,
modifiers, modifiers,
}) })
@ -650,8 +647,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 +775,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 +811,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 +833,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 {

View file

@ -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,
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -363,11 +363,11 @@ pub use {
input_state::{InputState, MultiTouchInfo, PointerState}, input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order}, layers::{LayerId, Order},
layout::*, layout::*,
memory::{Memory, 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},
@ -513,30 +513,18 @@ pub mod special_emojis {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WidgetType { pub enum WidgetType {
Label, // TODO(emilk): emit Label events Label, // TODO(emilk): emit Label events
/// e.g. a hyperlink /// e.g. a hyperlink
Link, Link,
TextEdit, TextEdit,
Button, Button,
Checkbox, Checkbox,
RadioButton, RadioButton,
SelectableLabel, SelectableLabel,
ComboBox, ComboBox,
Slider, Slider,
DragValue, DragValue,
ColorButton, ColorButton,
ImageButton, ImageButton,
CollapsingHeader, CollapsingHeader,
/// If you cannot fit any of the above slots. /// If you cannot fit any of the above slots.
@ -571,3 +559,25 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
pub fn accesskit_root_id() -> Id { pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root") Id::new("accesskit_root")
} }
/// Return a tree update that the egui integration should provide to the
/// AccessKit adapter if it cannot immediately run the egui application
/// to get a full tree update after running [`Context::enable_accesskit`].
#[cfg(feature = "accesskit")]
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
use accesskit::{Node, Role, Tree, TreeUpdate};
use std::sync::Arc;
let root_id = accesskit_root_id().accesskit_id();
TreeUpdate {
nodes: vec![(
root_id,
Arc::new(Node {
role: Role::Window,
..Default::default()
}),
)],
tree: Some(Tree::new(root_id)),
focus: None,
}
}

View file

@ -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.
@ -412,7 +405,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 {

View file

@ -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.
@ -68,7 +68,7 @@ fn set_menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0); style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE; style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE; style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.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;
} }
@ -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,28 +329,26 @@ 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; let mut in_old_menu = false;
let mut in_old_menu = false; if let Some(root) = root {
if let Some(root) = root { let menu_state = root.menu_state.read();
let menu_state = root.menu_state.read(); in_old_menu = menu_state.area_contains(pos);
in_old_menu = menu_state.area_contains(pos); destroy = root.id == response.id;
destroy = root.id == response.id; }
} if !in_old_menu {
if !in_old_menu { if response.hovered() && pointer.secondary_down() {
if response.hovered() && pointer.secondary_down() { return MenuResponse::Create(pos, id);
return MenuResponse::Create(pos, id); } else if (response.hovered() && pointer.primary_down()) || destroy {
} else if (response.hovered() && pointer.primary_down()) || destroy { return MenuResponse::Close;
return MenuResponse::Close;
}
} }
} }
} }
MenuResponse::Stay }
}) MenuResponse::Stay
} }
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) { fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
@ -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();
} }
} }

View file

@ -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`].

View file

@ -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,25 +173,23 @@ 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
// self.rect. See Context::interact. // self.rect. See Context::interact.
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true, // This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
// hence the extra complexity here. // hence the extra complexity here.
if self.hovered() { if self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
} else {
false // clicked without a pointer, weird
}
} else {
false false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
} else {
false // clicked without a pointer, weird
} }
}) } else {
false
}
} }
/// Was the widget enabled? /// 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,7 +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 self
} }
@ -496,7 +462,7 @@ impl Response {
/// When hovered or dragged, use this icon for the mouse cursor. /// When hovered or dragged, use this icon for the mouse cursor.
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self { pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() || self.dragged() { if self.hovered() || self.dragged() {
self.ctx.set_cursor_icon(cursor); self.ctx.output().cursor_icon = cursor;
} }
self self
} }
@ -545,10 +511,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.
@ -573,47 +537,47 @@ impl Response {
self.output_event(event); self.output_event(event);
} else { } else {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| { if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(builder, make_info()); self.fill_accesskit_node_from_widget_info(&mut node, make_info());
}); }
} }
} }
pub fn output_event(&self, event: crate::output::OutputEvent) { pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| { if let Some(mut node) = self.ctx.accesskit_node(self.id) {
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone()); self.fill_accesskit_node_from_widget_info(&mut node, event.widget_info().clone());
}); }
self.ctx.output_mut(|o| o.events.push(event)); self.ctx.output().events.push(event);
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) { pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
builder.set_bounds(accesskit::Rect { node.bounds = Some(accesskit::kurbo::Rect {
x0: self.rect.min.x.into(), x0: self.rect.min.x.into(),
y0: self.rect.min.y.into(), y0: self.rect.min.y.into(),
x1: self.rect.max.x.into(), x1: self.rect.max.x.into(),
y1: self.rect.max.y.into(), y1: self.rect.max.y.into(),
}); });
if self.sense.focusable { if self.sense.focusable {
builder.add_action(accesskit::Action::Focus); node.focusable = true;
} }
if self.sense.click && builder.default_action_verb().is_none() { if self.sense.click && node.default_action_verb.is_none() {
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click); node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
} }
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info( fn fill_accesskit_node_from_widget_info(
&self, &self,
builder: &mut accesskit::NodeBuilder, node: &mut accesskit::Node,
info: crate::WidgetInfo, info: crate::WidgetInfo,
) { ) {
use crate::WidgetType; use crate::WidgetType;
use accesskit::{CheckedState, Role}; use accesskit::{CheckedState, Role};
self.fill_accesskit_node_common(builder); self.fill_accesskit_node_common(node);
builder.set_role(match info.typ { node.role = match info.typ {
WidgetType::Label => Role::StaticText, WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link, WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField, WidgetType::TextEdit => Role::TextField,
@ -628,18 +592,18 @@ impl Response {
WidgetType::DragValue => Role::SpinButton, WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell, WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown, WidgetType::Other => Role::Unknown,
}); };
if let Some(label) = info.label { if let Some(label) = info.label {
builder.set_name(label); node.name = Some(label.into());
} }
if let Some(value) = info.current_text_value { if let Some(value) = info.current_text_value {
builder.set_value(value); node.value = Some(value.into());
} }
if let Some(value) = info.value { if let Some(value) = info.value {
builder.set_numeric_value(value); node.numeric_value = Some(value);
} }
if let Some(selected) = info.selected { if let Some(selected) = info.selected {
builder.set_checked_state(if selected { node.checked_state = Some(if selected {
CheckedState::True CheckedState::True
} else { } else {
CheckedState::False CheckedState::False
@ -662,9 +626,9 @@ impl Response {
/// ``` /// ```
pub fn labelled_by(self, id: Id) -> Self { pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| { if let Some(mut node) = self.ctx.accesskit_node(self.id) {
builder.push_labelled_by(id.accesskit_id()); node.labelled_by.push(id.accesskit_id());
}); }
#[cfg(not(feature = "accesskit"))] #[cfg(not(feature = "accesskit"))]
{ {
let _ = id; let _ = id;
@ -713,7 +677,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 +708,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:
/// ///
/// ``` /// ```

View file

@ -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,
} }
@ -492,7 +481,6 @@ pub struct Visuals {
pub resize_corner_size: f32, pub resize_corner_size: f32,
pub text_cursor_width: f32, pub text_cursor_width: f32,
/// show where the text cursor would be if you clicked /// show where the text cursor would be if you clicked
pub text_cursor_preview: bool, pub text_cursor_preview: bool,
@ -505,17 +493,9 @@ pub struct Visuals {
/// Show a background behind collapsing headers. /// Show a background behind collapsing headers.
pub collapsing_header_frame: bool, pub collapsing_header_frame: bool,
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
pub indent_has_left_vline: bool,
/// Wether or not Grids and Tables should be striped by default /// Wether or not Grids and Tables should be striped by default
/// (have alternating rows differently colored). /// (have alternating rows differently colored).
pub striped: bool, pub striped: bool,
/// Show trailing color behind the circle of a [`Slider`]. Default is OFF.
///
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
pub slider_trailing_fill: bool,
} }
impl Visuals { impl Visuals {
@ -553,7 +533,7 @@ impl Visuals {
// TODO(emilk): replace with an alpha // TODO(emilk): replace with an alpha
#[inline(always)] #[inline(always)]
pub fn fade_out_to_color(&self) -> Color32 { pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill self.widgets.noninteractive.bg_fill
} }
/// Returned a "grayed out" version of the given color. /// Returned a "grayed out" version of the given color.
@ -586,9 +566,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 +582,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 +594,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`.
@ -693,7 +663,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(),
@ -715,7 +684,6 @@ impl Default for Spacing {
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing` indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
interact_size: vec2(40.0, 18.0), interact_size: vec2(40.0, 18.0),
slider_width: 100.0, slider_width: 100.0,
combo_width: 100.0,
text_edit_width: 280.0, text_edit_width: 280.0,
icon_width: 14.0, icon_width: 14.0,
icon_width_inner: 8.0, icon_width_inner: 8.0,
@ -723,7 +691,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,8 +717,8 @@ impl Visuals {
widgets: Widgets::default(), widgets: Widgets::default(),
selection: Selection::default(), selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255), hyperlink_color: Color32::from_rgb(90, 170, 255),
faint_bg_color: Color32::from_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
@ -772,11 +739,8 @@ impl Visuals {
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true, button_frame: true,
collapsing_header_frame: false, collapsing_header_frame: false,
indent_has_left_vline: true,
striped: false, striped: false,
slider_trailing_fill: false,
} }
} }
@ -787,8 +751,8 @@ impl Visuals {
widgets: Widgets::light(), widgets: Widgets::light(),
selection: Selection::light(), selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255), hyperlink_color: Color32::from_rgb(0, 155, 255),
faint_bg_color: Color32::from_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
@ -837,7 +801,6 @@ impl Widgets {
pub fn dark() -> Self { pub fn dark() -> Self {
Self { Self {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
bg_fill: Color32::from_gray(27), bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
@ -845,15 +808,13 @@ impl Widgets {
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 +822,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 +829,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,7 +841,6 @@ impl Widgets {
pub fn light() -> Self { pub fn light() -> Self {
Self { Self {
noninteractive: WidgetVisuals { noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(248),
bg_fill: Color32::from_gray(248), bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
@ -890,15 +848,13 @@ impl Widgets {
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 +862,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 +869,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 +895,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 +936,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 +984,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,7 +992,6 @@ 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;
@ -1074,10 +1012,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 +1020,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");
@ -1255,17 +1185,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);
@ -1339,15 +1265,12 @@ impl Visuals {
clip_rect_margin, clip_rect_margin,
button_frame, button_frame,
collapsing_header_frame, collapsing_header_frame,
indent_has_left_vline,
striped, striped,
slider_trailing_fill,
} = self; } = self;
ui.collapsing("Background Colors", |ui| { ui.collapsing("Background Colors", |ui| {
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons"); ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows"); ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels"); ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
@ -1399,15 +1322,9 @@ impl Visuals {
ui.checkbox(button_frame, "Button has a frame"); ui.checkbox(button_frame, "Button has a frame");
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame"); ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
ui.checkbox(
indent_has_left_vline,
"Paint a vertical line to the left of indented regions",
);
ui.checkbox(striped, "By default, add stripes to grids and tables?"); ui.checkbox(striped, "By default, add stripes to grids and tables?");
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
ui.vertical_centered(|ui| reset_button(ui, self)); ui.vertical_centered(|ui| reset_button(ui, self));
} }
} }

View file

@ -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, containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
util::IdTypeMap, widgets::*, *, widgets::*, *,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -314,9 +314,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 +413,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 +961,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 +990,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 +1022,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 +1563,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,31 +1780,24 @@ 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); }
}
let stroke = self.visuals().widgets.noninteractive.bg_stroke; // draw a faint line on the left to mark the indented section
let left_top = child_rect.min - 0.5 * indent * Vec2::X; let stroke = self.visuals().widgets.noninteractive.bg_stroke;
let left_top = self.painter().round_pos_to_pixels(left_top); let left_top = child_rect.min - 0.5 * indent * Vec2::X;
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0); let left_top = self.painter().round_pos_to_pixels(left_top);
let left_bottom = self.painter().round_pos_to_pixels(left_bottom); 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);
if left_vline { self.painter.line_segment([left_top, left_bottom], stroke);
// draw a faint line on the left to mark the indented section if end_with_horizontal_line {
self.painter.line_segment([left_top, left_bottom], stroke); let fudge = 2.0; // looks nicer with button rounding in collapsing headers
} let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
self.painter
if end_with_horizontal_line { .line_segment([left_bottom, right_bottom], stroke);
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
self.painter
.line_segment([left_bottom, right_bottom], stroke);
}
} }
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
@ -2173,43 +2157,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)
}
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -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()

View file

@ -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,
} }
} }

View file

@ -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 {

View file

@ -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();
} }
} }
@ -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()))
} }

View file

@ -368,35 +368,33 @@ 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. // The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id(); let id = ui.next_auto_id();
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id)); let is_slow_speed = shift && ui.memory().is_being_dragged(id);
// The following ensures that when a `DragValue` receives focus, // 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(id);
mem.interested_in_focus(id); let is_kb_editing = ui.memory().has_focus(id);
let is_kb_editing = mem.has_focus(id); if ui.memory().gained_focus(id) {
if mem.gained_focus(id) { ui.memory().drag_value.edit_string = None;
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 change = {
let mut change = 0.0; let mut change = 0.0;
let mut input = ui.input_mut();
if is_kb_editing { if is_kb_editing {
// This deliberately doesn't listen for left and right arrow keys, // This deliberately doesn't listen for left and right arrow keys,
@ -420,18 +418,16 @@ impl<'a> Widget for DragValue<'a> {
} }
change change
}); };
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
{ {
use accesskit::{Action, ActionData}; use accesskit::{Action, ActionData};
ui.input(|input| { for request in ui.input().accesskit_action_requests(id, Action::SetValue) {
for request in input.accesskit_action_requests(id, Action::SetValue) { if let Some(ActionData::NumericValue(new_value)) = request.data {
if let Some(ActionData::NumericValue(new_value)) = request.data { value = new_value;
value = new_value;
}
} }
}); }
} }
if change != 0.0 { if change != 0.0 {
@ -442,7 +438,7 @@ impl<'a> Widget for DragValue<'a> {
value = clamp_to_range(value, clamp_range.clone()); value = clamp_to_range(value, clamp_range.clone());
if old_value != value { if old_value != value {
set(&mut get_set_value, value); set(&mut get_set_value, value);
ui.memory_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 +452,35 @@ impl<'a> Widget for DragValue<'a> {
} }
}; };
let text_style = ui.style().drag_value_text_style.clone();
// some clones below are redundant if AccessKit is disabled // some clones below are redundant if AccessKit is disabled
#[allow(clippy::redundant_clone)] #[allow(clippy::redundant_clone)]
let mut response = if is_kb_editing { let mut response = if is_kb_editing {
let button_width = ui.spacing().interact_size.x;
let mut value_text = ui let mut value_text = ui
.memory_mut(|mem| mem.drag_value.edit_string.take()) .memory()
.drag_value
.edit_string
.take()
.unwrap_or_else(|| value_text.clone()); .unwrap_or_else(|| value_text.clone());
let response = ui.add( let response = ui.add(
TextEdit::singleline(&mut value_text) TextEdit::singleline(&mut value_text)
.clip_text(false)
.horizontal_align(ui.layout().horizontal_align())
.vertical_align(ui.layout().vertical_align())
.margin(ui.spacing().button_padding)
.min_size(ui.spacing().interact_size)
.id(id) .id(id)
.desired_width(ui.spacing().interact_size.x) .desired_width(button_width)
.font(text_style), .font(TextStyle::Monospace),
); );
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame. let parsed_value = match custom_parser {
// See https://github.com/emilk/egui/issues/2687 Some(parser) => parser(&value_text),
if response.lost_focus() { None => value_text.parse().ok(),
let parsed_value = match custom_parser { };
Some(parser) => parser(&value_text), if let Some(parsed_value) = parsed_value {
None => value_text.parse().ok(), let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
}; set(&mut get_set_value, parsed_value);
if let Some(parsed_value) = parsed_value {
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
set(&mut get_set_value, parsed_value);
}
} }
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text)); ui.memory().drag_value.edit_string = Some(value_text);
response response
} else { } else {
let button = Button::new( let button = Button::new(
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)) RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)).monospace(),
.text_style(text_style),
) )
.wrap(false) .wrap(false)
.sense(Sense::click_and_drag()) .sense(Sense::click_and_drag())
@ -511,18 +499,10 @@ impl<'a> Widget for DragValue<'a> {
} }
if response.clicked() { if response.clicked() {
ui.memory_mut(|mem| { ui.memory().drag_value.edit_string = None;
mem.drag_value.edit_string = None; ui.memory().request_focus(id);
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 +512,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))
@ -553,7 +533,7 @@ impl<'a> Widget for DragValue<'a> {
drag_state.last_dragged_id = Some(response.id); drag_state.last_dragged_id = Some(response.id);
drag_state.last_dragged_value = Some(stored_value); drag_state.last_dragged_value = Some(stored_value);
ui.memory_mut(|mem| mem.drag_value = drag_state); ui.memory().drag_value = drag_state;
} }
} }
@ -565,28 +545,28 @@ impl<'a> Widget for DragValue<'a> {
response.widget_info(|| WidgetInfo::drag_value(value)); response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
ui.ctx().accesskit_node_builder(response.id, |builder| { if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
use accesskit::Action; use accesskit::Action;
// If either end of the range is unbounded, it's better // If either end of the range is unbounded, it's better
// to leave the corresponding AccessKit field set to None, // to leave the corresponding AccessKit field set to None,
// to allow for platform-specific default behavior. // to allow for platform-specific default behavior.
if clamp_range.start().is_finite() { if clamp_range.start().is_finite() {
builder.set_min_numeric_value(*clamp_range.start()); node.min_numeric_value = Some(*clamp_range.start());
} }
if clamp_range.end().is_finite() { if clamp_range.end().is_finite() {
builder.set_max_numeric_value(*clamp_range.end()); node.max_numeric_value = Some(*clamp_range.end());
} }
builder.set_numeric_value_step(speed); node.numeric_value_step = Some(speed);
builder.add_action(Action::SetValue); node.actions |= Action::SetValue;
if value < *clamp_range.end() { if value < *clamp_range.end() {
builder.add_action(Action::Increment); node.actions |= Action::Increment;
} }
if value > *clamp_range.start() { if value > *clamp_range.start() {
builder.add_action(Action::Decrement); node.actions |= Action::Decrement;
} }
// The name field is set to the current value by the button, // The name field is set to the current value by the button,
// but we don't want it set that way on this widget type. // but we don't want it set that way on this widget type.
builder.clear_name(); node.name = None;
// Always expose the value as a string. This makes the widget // Always expose the value as a string. This makes the widget
// more stable to accessibility users as it switches // more stable to accessibility users as it switches
// between edit and button modes. This is particularly important // between edit and button modes. This is particularly important
@ -607,9 +587,9 @@ impl<'a> Widget for DragValue<'a> {
// when in edit mode. // when in edit mode.
if !is_kb_editing { if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix); let value_text = format!("{}{}{}", prefix, value_text, suffix);
builder.set_value(value_text); node.value = Some(value_text.into());
} }
}); }
response response
} }

View file

@ -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) {
@ -110,20 +110,16 @@ 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)

View file

@ -72,7 +72,7 @@ impl Label {
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) { pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let sense = self.sense.unwrap_or_else(|| { let sense = self.sense.unwrap_or_else(|| {
// We only want to focus labels if the screen reader is on. // We only want to focus labels if the screen reader is on.
if ui.memory(|mem| mem.options.screen_reader) { if ui.memory().options.screen_reader {
Sense::focusable_noninteractive() Sense::focusable_noninteractive()
} else { } else {
Sense::hover() Sense::hover()
@ -120,7 +120,7 @@ impl Label {
if let Some(first_section) = text_job.job.sections.first_mut() { if let Some(first_section) = text_job.job.sections.first_mut() {
first_section.leading_space = first_row_indentation; first_section.leading_space = first_row_indentation;
} }
let text_galley = 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!(
@ -153,7 +153,7 @@ impl Label {
text_job.job.justify = ui.layout().horizontal_justify(); text_job.job.justify = ui.layout().horizontal_justify();
}; };
let text_galley = 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(), sense);
let pos = match text_galley.galley.job.halign { let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(), Align::LEFT => rect.left_top(),
@ -173,7 +173,7 @@ impl Widget for Label {
if ui.is_rect_visible(response.rect) { if ui.is_rect_visible(response.rect) {
let response_color = ui.style().interact(&response).text_color(); let response_color = ui.style().interact(&response).text_color();
let underline = if response.has_focus() || 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

View file

@ -1658,16 +1658,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( &plot.ui.fonts(),
f, 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.
@ -1716,16 +1714,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>(

View file

@ -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;

View file

@ -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);
} }
} }
@ -742,7 +742,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 +754,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 +762,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 +953,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 +963,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();
@ -1044,7 +1042,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 +1069,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 +1094,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)
} }

View file

@ -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;
@ -99,8 +90,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 +98,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 +116,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 {

View file

@ -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);

View file

@ -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;
@ -106,14 +75,14 @@ impl Widget for Separator {
let painter = ui.painter(); let painter = ui.painter();
if is_horizontal_line { if is_horizontal_line {
painter.hline( painter.hline(
(rect.left() - grow)..=(rect.right() + grow), rect.x_range(),
painter.round_to_pixel(rect.center().y), painter.round_to_pixel(rect.center().y),
stroke, stroke,
); );
} else { } else {
painter.vline( painter.vline(
painter.round_to_pixel(rect.center().x), painter.round_to_pixel(rect.center().x),
(rect.top() - grow)..=(rect.bottom() + grow), rect.y_range(),
stroke, stroke,
); );
} }

View file

@ -84,7 +84,6 @@ pub struct Slider<'a> {
max_decimals: Option<usize>, max_decimals: Option<usize>,
custom_formatter: Option<NumFormatter<'a>>, custom_formatter: Option<NumFormatter<'a>>,
custom_parser: Option<NumParser<'a>>, custom_parser: Option<NumParser<'a>>,
trailing_fill: Option<bool>,
} }
impl<'a> Slider<'a> { impl<'a> Slider<'a> {
@ -130,7 +129,6 @@ impl<'a> Slider<'a> {
max_decimals: None, max_decimals: None,
custom_formatter: None, custom_formatter: None,
custom_parser: None, custom_parser: None,
trailing_fill: None,
} }
} }
@ -271,17 +269,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
@ -553,7 +540,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()),
@ -575,19 +562,19 @@ impl<'a> Slider<'a> {
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown), SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
}; };
ui.input(|input| { decrement += ui.input().num_presses(dec_key);
decrement += input.num_presses(dec_key); increment += ui.input().num_presses(inc_key);
increment += input.num_presses(inc_key);
});
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
{ {
use accesskit::Action; use accesskit::Action;
ui.input(|input| { decrement += ui
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement); .input()
increment += input.num_accesskit_action_requests(response.id, Action::Increment); .num_accesskit_action_requests(response.id, Action::Decrement);
}); increment += ui
.input()
.num_accesskit_action_requests(response.id, Action::Increment);
} }
let kb_step = increment as f32 - decrement as f32; let kb_step = increment as f32 - decrement as f32;
@ -599,7 +586,7 @@ impl<'a> Slider<'a> {
let new_value = match self.step { let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step), Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => { None if self.smart_aim => {
let aim_radius = ui.input(|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(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()), self.value_from_position(new_position + aim_radius, position_range.clone()),
@ -613,13 +600,14 @@ impl<'a> Slider<'a> {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
{ {
use accesskit::{Action, ActionData}; use accesskit::{Action, ActionData};
ui.input(|input| { for request in ui
for request in input.accesskit_action_requests(response.id, Action::SetValue) { .input()
if let Some(ActionData::NumericValue(new_value)) = request.data { .accesskit_action_requests(response.id, Action::SetValue)
self.set_value(new_value); {
} if let Some(ActionData::NumericValue(new_value)) = request.data {
self.set_value(new_value);
} }
}); }
} }
// Paint it: // Paint it:
@ -629,40 +617,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,
@ -727,12 +697,14 @@ 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` let change = {
let change = ui.input(|input| { // 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 any_change = change != 0;
let speed = if let (Some(step), true) = (self.step, any_change) { let speed = if let (Some(step), true) = (self.step, any_change) {
@ -792,22 +764,20 @@ impl<'a> Slider<'a> {
response.widget_info(|| WidgetInfo::slider(value, self.text.text())); response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
ui.ctx().accesskit_node_builder(response.id, |builder| { if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
use accesskit::Action; use accesskit::Action;
builder.set_min_numeric_value(*self.range.start()); node.min_numeric_value = Some(*self.range.start());
builder.set_max_numeric_value(*self.range.end()); node.max_numeric_value = Some(*self.range.end());
if let Some(step) = self.step { node.numeric_value_step = self.step;
builder.set_numeric_value_step(step); node.actions |= Action::SetValue;
}
builder.add_action(Action::SetValue);
let clamp_range = self.clamp_range(); let clamp_range = self.clamp_range();
if value < *clamp_range.end() { if value < *clamp_range.end() {
builder.add_action(Action::Increment); node.actions |= Action::Increment;
} }
if value > *clamp_range.start() { if value > *clamp_range.start() {
builder.add_action(Action::Decrement); node.actions |= Action::Decrement;
} }
}); }
let slider_response = response.clone(); let slider_response = response.clone();

View file

@ -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);

View file

@ -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());
});
} }
} }
} }
@ -713,100 +659,89 @@ impl<'t> TextEdit<'t> {
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
{ if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| { use accesskit::{Role, TextDirection, TextPosition, TextSelection};
use accesskit::{TextPosition, TextSelection};
let parent_id = response.id; let parent_id = response.id;
if let Some(cursor_range) = &cursor_range { if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor; let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor; let focus = &cursor_range.primary.rcursor;
builder.set_text_selection(TextSelection { node.text_selection = Some(TextSelection {
anchor: TextPosition { anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(), node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column, character_index: anchor.column,
}, },
focus: TextPosition { focus: TextPosition {
node: parent_id.with(focus.row).accesskit_id(), node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column, character_index: focus.column,
}, },
});
}
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
if self.multiline {
builder.set_multiline();
}
parent_id
});
if let Some(parent_id) = parent_id {
// drop ctx lock before further processing
use accesskit::{Role, TextDirection};
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
ui.ctx().accesskit_node_builder(id, |builder| {
builder.set_role(Role::InlineTextBox);
let rect = row.rect.translate(text_draw_pos.to_vec2());
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
builder.set_text_direction(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let glyph_count = row.glyphs.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::new();
character_lengths.reserve(glyph_count);
let mut character_positions = Vec::<f32>::new();
character_positions.reserve(glyph_count);
let mut character_widths = Vec::<f32>::new();
character_widths.reserve(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths
.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.rect.min.x);
character_widths.push(glyph.size.x);
}
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.rect.max.x - row.rect.min.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
builder.set_value(value);
builder.set_character_lengths(character_lengths);
builder.set_character_positions(character_positions);
builder.set_character_widths(character_widths);
builder.set_word_lengths(word_lengths);
});
}
}); });
} }
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
node.multiline = self.multiline;
drop(node);
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
let mut node = ui.ctx().accesskit_node(id).unwrap();
node.role = Role::InlineTextBox;
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
node.text_direction = Some(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let glyph_count = row.glyphs.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::new();
character_lengths.reserve(glyph_count);
let mut character_positions = Vec::<f32>::new();
character_positions.reserve(glyph_count);
let mut character_widths = Vec::<f32>::new();
character_widths.reserve(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.rect.min.x);
character_widths.push(glyph.size.x);
}
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.rect.max.x - row.rect.min.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
node.value = Some(value.into());
node.character_lengths = character_lengths.into();
node.character_positions = Some(character_positions.into());
node.character_widths = Some(character_widths.into());
node.word_lengths = word_lengths.into();
}
});
} }
TextEditOutput { TextEditOutput {
@ -877,19 +812,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 => {
@ -934,7 +869,7 @@ fn events(
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 +893,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;
} }
} }
@ -1062,7 +997,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()),
); );

View file

@ -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.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_demo_app" name = "egui_demo_app"
version = "0.21.0" version = "0.20.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.20.0", path = "../eframe", default-features = false }
egui = { version = "0.21.0", path = "../egui", features = [ egui = { version = "0.20.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.20.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.20.0", optional = true, path = "../egui_extras" }
# feature "http": # feature "http":
ehttp = { version = "0.2.0", optional = true } ehttp = { version = "0.2.0", optional = true }

View file

@ -1,8 +1,8 @@
use std::{num::NonZeroU64, sync::Arc}; use std::{num::NonZeroU64, sync::Arc};
use eframe::{ use eframe::{
egui_wgpu::wgpu::util::DeviceExt,
egui_wgpu::{self, wgpu}, egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
}; };
pub struct Custom3d { pub struct Custom3d {

View file

@ -35,7 +35,7 @@ impl Default for FractalClock {
impl FractalClock { impl FractalClock {
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) { pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
if !self.paused { if !self.paused {
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time)); self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }

View file

@ -131,7 +131,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
trigger_fetch = true; trigger_fetch = true;
} }
if ui.button("Random image").clicked() { if ui.button("Random image").clicked() {
let seed = ui.input(|i| i.time); let seed = ui.input().time;
let side = 640; let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side); *url = format!("https://picsum.photos/seed/{}/{}", seed, side);
trigger_fetch = true; trigger_fetch = true;
@ -187,7 +187,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
if let Some(text) = &text { if let Some(text) = &text {
let tooltip = "Click to copy the response body"; let tooltip = "Click to copy the response body";
if ui.button("📋").on_hover_text(tooltip).clicked() { if ui.button("📋").on_hover_text(tooltip).clicked() {
ui.output_mut(|o| o.copied_text = text.clone()); ui.output().copied_text = text.clone();
} }
ui.separator(); ui.separator();
} }
@ -245,7 +245,7 @@ impl ColoredText {
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| { let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
let mut layout_job = self.0.clone(); let mut layout_job = self.0.clone();
layout_job.wrap.max_width = wrap_width; layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job)) ui.fonts().layout_job(layout_job)
}; };
let mut text = self.0.text.as_str(); let mut text = self.0.text.as_str();
@ -258,7 +258,7 @@ impl ColoredText {
} else { } else {
let mut job = self.0.clone(); let mut job = self.0.clone();
job.wrap.max_width = ui.available_width(); job.wrap.max_width = ui.available_width();
let galley = ui.fonts(|f| f.layout_job(job)); let galley = ui.fonts().layout_job(job);
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover()); let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
painter.add(egui::Shape::galley(response.rect.min, galley)); painter.add(egui::Shape::galley(response.rect.min, galley));
} }

View file

@ -81,7 +81,7 @@ impl Default for BackendPanel {
impl BackendPanel { impl BackendPanel {
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.frame_history self.frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); .on_new_frame(ctx.input().time, frame.info().cpu_usage);
match self.run_mode { match self.run_mode {
RunMode::Continuous => { RunMode::Continuous => {
@ -130,12 +130,10 @@ impl BackendPanel {
ui.separator(); ui.separator();
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "web_screen-reader")]
{ {
let mut screen_reader = ui.ctx().options(|o| o.screen_reader); let mut screen_reader = ui.ctx().options().screen_reader;
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms"); ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options_mut(|o| o.screen_reader = screen_reader); ui.ctx().options().screen_reader = screen_reader;
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -341,11 +339,9 @@ impl EguiWindows {
output_event_history, output_event_history,
} = self; } = self;
ctx.output(|o| { for event in &ctx.output().events {
for event in &o.events { output_event_history.push_back(event.clone());
output_event_history.push_back(event.clone()); }
}
});
while output_event_history.len() > 1000 { while output_event_history.len() > 1000 {
output_event_history.pop_front(); output_event_history.pop_front();
} }

View file

@ -95,21 +95,19 @@ impl FrameHistory {
)); ));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage); let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| { shapes.push(Shape::text(
Shape::text( &ui.fonts(),
f, pos2(rect.left(), y),
pos2(rect.left(), y), egui::Align2::LEFT_BOTTOM,
egui::Align2::LEFT_BOTTOM, text,
text, TextStyle::Monospace.resolve(ui.style()),
TextStyle::Monospace.resolve(ui.style()), color,
color, ));
)
}));
} }
let circle_color = color; let circle_color = color;
let radius = 2.0; let radius = 2.0;
let right_side_time = ui.input(|i| i.time); // Time at right side of screen let right_side_time = ui.input().time; // Time at right side of screen
for (time, cpu_usage) in history.iter() { for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32; let age = (right_side_time - time) as f32;

View file

@ -175,8 +175,8 @@ impl eframe::App for WrapApp {
eframe::set_value(storage, eframe::APP_KEY, &self.state); eframe::set_value(storage, eframe::APP_KEY, &self.state);
} }
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
visuals.panel_fill.to_normalized_gamma_f32() visuals.panel_fill.into()
} }
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
@ -191,7 +191,10 @@ impl eframe::App for WrapApp {
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) { if ctx
.input_mut()
.consume_key(egui::Modifiers::NONE, egui::Key::F11)
{
frame.set_fullscreen(!frame.info().window_info.fullscreen); frame.set_fullscreen(!frame.info().window_info.fullscreen);
} }
@ -238,8 +241,7 @@ impl WrapApp {
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// The backend-panel can be toggled on/off. // The backend-panel can be toggled on/off.
// We show a little animation when the user switches it. // We show a little animation when the user switches it.
let is_open = let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible();
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
egui::SidePanel::left("backend_panel") egui::SidePanel::left("backend_panel")
.resizable(false) .resizable(false)
@ -264,13 +266,13 @@ impl WrapApp {
.on_hover_text("Forget scroll, positions, sizes etc") .on_hover_text("Forget scroll, positions, sizes etc")
.clicked() .clicked()
{ {
ui.ctx().memory_mut(|mem| *mem = Default::default()); *ui.ctx().memory() = Default::default();
ui.close_menu(); ui.close_menu();
} }
if ui.button("Reset everything").clicked() { if ui.button("Reset everything").clicked() {
self.state = Default::default(); self.state = Default::default();
ui.ctx().memory_mut(|mem| *mem = Default::default()); *ui.ctx().memory() = Default::default();
ui.close_menu(); ui.close_menu();
} }
}); });
@ -280,7 +282,7 @@ impl WrapApp {
let mut found_anchor = false; let mut found_anchor = false;
let selected_anchor = self.state.selected_anchor.clone(); let selected_anchor = self.state.selected_anchor.clone();
for (_name, anchor, app) in self.apps_iter_mut() { for (_name, anchor, app) in self.apps_iter_mut() {
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) { if anchor == selected_anchor || ctx.memory().everything_is_visible() {
app.update(ctx, frame); app.update(ctx, frame);
found_anchor = true; found_anchor = true;
} }
@ -314,7 +316,7 @@ impl WrapApp {
{ {
selected_anchor = anchor.to_owned(); selected_anchor = anchor.to_owned();
if frame.is_web() { if frame.is_web() {
ui.output_mut(|o| o.open_url(format!("#{}", anchor))); ui.output().open_url(format!("#{}", anchor));
} }
} }
} }
@ -326,7 +328,7 @@ impl WrapApp {
if clock_button(ui, crate::seconds_since_midnight()).clicked() { if clock_button(ui, crate::seconds_since_midnight()).clicked() {
self.state.selected_anchor = "clock".to_owned(); self.state.selected_anchor = "clock".to_owned();
if frame.is_web() { if frame.is_web() {
ui.output_mut(|o| o.open_url("#clock")); ui.output().open_url("#clock");
} }
} }
} }
@ -340,25 +342,22 @@ impl WrapApp {
use std::fmt::Write as _; use std::fmt::Write as _;
// Preview hovering files: // Preview hovering files:
if !ctx.input(|i| i.raw.hovered_files.is_empty()) { if !ctx.input().raw.hovered_files.is_empty() {
let text = ctx.input(|i| { let mut text = "Dropping files:\n".to_owned();
let mut text = "Dropping files:\n".to_owned(); for file in &ctx.input().raw.hovered_files {
for file in &i.raw.hovered_files { if let Some(path) = &file.path {
if let Some(path) = &file.path { write!(text, "\n{}", path.display()).ok();
write!(text, "\n{}", path.display()).ok(); } else if !file.mime.is_empty() {
} else if !file.mime.is_empty() { write!(text, "\n{}", file.mime).ok();
write!(text, "\n{}", file.mime).ok(); } else {
} else { text += "\n???";
text += "\n???";
}
} }
text }
});
let painter = let painter =
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = ctx.screen_rect(); let screen_rect = ctx.input().screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192)); painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text( painter.text(
screen_rect.center(), screen_rect.center(),
@ -370,11 +369,9 @@ impl WrapApp {
} }
// Collect dropped files: // Collect dropped files:
ctx.input(|i| { if !ctx.input().raw.dropped_files.is_empty() {
if !i.raw.dropped_files.is_empty() { self.dropped_files = ctx.input().raw.dropped_files.clone();
self.dropped_files = i.raw.dropped_files.clone(); }
}
});
// Show dropped files (if any): // Show dropped files (if any):
if !self.dropped_files.is_empty() { if !self.dropped_files.is_empty() {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "egui_demo_lib" name = "egui_demo_lib"
version = "0.21.0" version = "0.20.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Example library for egui" description = "Example library for egui"
edition = "2021" edition = "2021"
@ -30,8 +30,8 @@ syntax_highlighting = ["syntect"]
[dependencies] [dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false } egui = { version = "0.20.0", path = "../egui", default-features = false }
egui_extras = { version = "0.21.0", path = "../egui_extras" } egui_extras = { version = "0.20.0", path = "../egui_extras" }
enum-map = { version = "2", features = ["serde"] } enum-map = { version = "2", features = ["serde"] }
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
unicode_names2 = { version = "0.6.0", default-features = false } unicode_names2 = { version = "0.6.0", default-features = false }

View file

@ -38,7 +38,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
if false { if false {
let ctx = egui::Context::default(); let ctx = egui::Context::default();
ctx.memory_mut(|m| m.set_everything_is_visible(true)); // give us everything ctx.memory().set_everything_is_visible(true); // give us everything
let mut demo_windows = egui_demo_lib::DemoWindows::default(); let mut demo_windows = egui_demo_lib::DemoWindows::default();
c.bench_function("demo_full_no_tessellate", |b| { c.bench_function("demo_full_no_tessellate", |b| {
b.iter(|| { b.iter(|| {

View file

@ -79,7 +79,7 @@ impl super::View for CodeEditor {
let mut layout_job = let mut layout_job =
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
layout_job.wrap.max_width = wrap_width; layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job)) ui.fonts().layout_job(layout_job)
}; };
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {

View file

@ -103,7 +103,7 @@ impl CodeExample {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let font_id = egui::TextStyle::Monospace.resolve(ui.style()); let font_id = egui::TextStyle::Monospace.resolve(ui.style());
let indentation = 8.0 * ui.fonts(|f| f.glyph_width(&font_id, ' ')); let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
let item_spacing = ui.spacing_mut().item_spacing; let item_spacing = ui.spacing_mut().item_spacing;
ui.add_space(indentation - item_spacing.x); ui.add_space(indentation - item_spacing.x);

View file

@ -30,7 +30,7 @@ impl super::View for DancingStrings {
Frame::canvas(ui.style()).show(ui, |ui| { Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
let time = ui.input(|i| i.time); let time = ui.input().time;
let desired_size = ui.available_width() * vec2(1.0, 0.35); let desired_size = ui.available_width() * vec2(1.0, 0.35);
let (_id, rect) = ui.allocate_space(desired_size); let (_id, rect) = ui.allocate_space(desired_size);

View file

@ -90,7 +90,6 @@ impl Default for Tests {
fn default() -> Self { fn default() -> Self {
Self::from_demos(vec![ Self::from_demos(vec![
Box::new(super::tests::CursorTest::default()), Box::new(super::tests::CursorTest::default()),
Box::new(super::highlighting::Highlighting::default()),
Box::new(super::tests::IdTest::default()), Box::new(super::tests::IdTest::default()),
Box::new(super::tests::InputTest::default()), Box::new(super::tests::InputTest::default()),
Box::new(super::layout_test::LayoutTest::default()), Box::new(super::layout_test::LayoutTest::default()),
@ -178,7 +177,7 @@ impl DemoWindows {
fn mobile_ui(&mut self, ctx: &Context) { fn mobile_ui(&mut self, ctx: &Context) {
if self.about_is_open { if self.about_is_open {
let screen_size = ctx.input(|i| i.screen_rect.size()); let screen_size = ctx.input().screen_rect.size();
let default_width = (screen_size.x - 20.0).min(400.0); let default_width = (screen_size.x - 20.0).min(400.0);
let mut close = false; let mut close = false;
@ -217,7 +216,7 @@ impl DemoWindows {
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| { ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`. ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
self.demo_list_ui(ui); self.demo_list_ui(ui);
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) { if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
ui.close_menu(); ui.close_menu();
} }
}); });
@ -292,7 +291,7 @@ impl DemoWindows {
ui.separator(); ui.separator();
if ui.button("Organize windows").clicked() { if ui.button("Organize windows").clicked() {
ui.ctx().memory_mut(|mem| mem.reset_areas()); ui.ctx().memory().reset_areas();
} }
}); });
}); });
@ -310,12 +309,12 @@ fn file_menu_button(ui: &mut Ui) {
// NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu, // NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu,
// or else they would only be checked if the "File" menu was actually open! // or else they would only be checked if the "File" menu was actually open!
if ui.input_mut(|i| i.consume_shortcut(&organize_shortcut)) { if ui.input_mut().consume_shortcut(&organize_shortcut) {
ui.ctx().memory_mut(|mem| mem.reset_areas()); ui.ctx().memory().reset_areas();
} }
if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) { if ui.input_mut().consume_shortcut(&reset_shortcut) {
ui.ctx().memory_mut(|mem| *mem = Default::default()); *ui.ctx().memory() = Default::default();
} }
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
@ -336,7 +335,7 @@ fn file_menu_button(ui: &mut Ui) {
) )
.clicked() .clicked()
{ {
ui.ctx().memory_mut(|mem| mem.reset_areas()); ui.ctx().memory().reset_areas();
ui.close_menu(); ui.close_menu();
} }
@ -348,7 +347,7 @@ fn file_menu_button(ui: &mut Ui) {
.on_hover_text("Forget scroll, positions, sizes etc") .on_hover_text("Forget scroll, positions, sizes etc")
.clicked() .clicked()
{ {
ui.ctx().memory_mut(|mem| *mem = Default::default()); *ui.ctx().memory() = Default::default();
ui.close_menu(); ui.close_menu();
} }
}); });

View file

@ -1,7 +1,7 @@
use egui::*; use egui::*;
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)); let is_being_dragged = ui.memory().is_being_dragged(id);
if !is_being_dragged { if !is_being_dragged {
let response = ui.scope(body).response; let response = ui.scope(body).response;
@ -9,10 +9,10 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
// Check for drags: // Check for drags:
let response = ui.interact(response.rect, id, Sense::drag()); let response = ui.interact(response.rect, id, Sense::drag());
if response.hovered() { if response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Grab); ui.output().cursor_icon = CursorIcon::Grab;
} }
} else { } else {
ui.ctx().set_cursor_icon(CursorIcon::Grabbing); ui.output().cursor_icon = CursorIcon::Grabbing;
// Paint the body to a new layer: // Paint the body to a new layer:
let layer_id = LayerId::new(Order::Tooltip, id); let layer_id = LayerId::new(Order::Tooltip, id);
@ -37,7 +37,7 @@ pub fn drop_target<R>(
can_accept_what_is_being_dragged: bool, can_accept_what_is_being_dragged: bool,
body: impl FnOnce(&mut Ui) -> R, body: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); let is_being_dragged = ui.memory().is_anything_being_dragged();
let margin = Vec2::splat(4.0); let margin = Vec2::splat(4.0);
@ -139,7 +139,7 @@ impl super::View for DragAndDropDemo {
}); });
}); });
if ui.memory(|mem| mem.is_being_dragged(item_id)) { if ui.memory().is_being_dragged(item_id) {
source_col_row = Some((col_idx, row_idx)); source_col_row = Some((col_idx, row_idx));
} }
} }
@ -153,7 +153,7 @@ impl super::View for DragAndDropDemo {
} }
}); });
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); let is_being_dragged = ui.memory().is_anything_being_dragged();
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() { if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
drop_col = Some(col_idx); drop_col = Some(col_idx);
} }
@ -162,7 +162,7 @@ impl super::View for DragAndDropDemo {
if let Some((source_col, source_row)) = source_col_row { if let Some((source_col, source_row)) = source_col_row {
if let Some(drop_col) = drop_col { if let Some(drop_col) = drop_col {
if ui.input(|i| i.pointer.any_released()) { if ui.input().pointer.any_released() {
// do the drop: // do the drop:
let item = self.columns[source_col].remove(source_row); let item = self.columns[source_col].remove(source_row);
self.columns[drop_col].push(item); self.columns[drop_col].push(item);

View file

@ -93,7 +93,7 @@ impl super::View for FontBook {
}; };
if ui.add(button).on_hover_ui(tooltip_ui).clicked() { if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
ui.output_mut(|o| o.copied_text = chr.to_string()); ui.output().copied_text = chr.to_string();
} }
} }
} }
@ -103,16 +103,15 @@ impl super::View for FontBook {
} }
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> { fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
ui.fonts(|f| { ui.fonts()
f.lock() .lock()
.fonts .fonts
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters .font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
.characters() .characters()
.iter() .iter()
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control()) .filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
.map(|&chr| (chr, char_name(chr))) .map(|&chr| (chr, char_name(chr)))
.collect() .collect()
})
} }
fn char_name(chr: char) -> String { fn char_name(chr: char) -> String {

View file

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

View file

@ -202,7 +202,7 @@ impl Widgets {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below: // Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' ')); let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' ');
ui.spacing_mut().item_spacing.x = width; ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110))); ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));

View file

@ -12,7 +12,6 @@ pub mod dancing_strings;
pub mod demo_app_windows; pub mod demo_app_windows;
pub mod drag_and_drop; pub mod drag_and_drop;
pub mod font_book; pub mod font_book;
pub mod highlighting;
pub mod layout_test; pub mod layout_test;
pub mod misc_demo_window; pub mod misc_demo_window;
pub mod multi_touch; pub mod multi_touch;

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